@yetter/client 0.0.1
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 +0 -0
- package/dist/api.d.ts +11 -0
- package/dist/api.js +75 -0
- package/dist/client.d.ts +10 -0
- package/dist/client.js +261 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/types.d.ts +89 -0
- package/dist/types.js +1 -0
- package/examples/stream.ts +53 -0
- package/examples/submit.ts +79 -0
- package/examples/subscribe.ts +39 -0
- package/package.json +39 -0
- package/src/api.ts +105 -0
- package/src/client.ts +312 -0
- package/src/index.ts +3 -0
- package/src/types.ts +112 -0
- package/tsconfig.json +13 -0
package/README.md
ADDED
|
File without changes
|
package/dist/api.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { ClientOptions, GenerateImageRequest, GenerateImageResponse, GetStatusRequest, GetStatusResponse, CancelRequest, CancelResponse, GetResponseRequest, GetResponseResponse } from "./types";
|
|
2
|
+
export declare class YetterImageClient {
|
|
3
|
+
private apiKey;
|
|
4
|
+
private endpoint;
|
|
5
|
+
constructor(options: ClientOptions);
|
|
6
|
+
getApiEndpoint(): string;
|
|
7
|
+
generateImage(body: GenerateImageRequest): Promise<GenerateImageResponse>;
|
|
8
|
+
getStatus(body: GetStatusRequest): Promise<GetStatusResponse>;
|
|
9
|
+
cancel(body: CancelRequest): Promise<CancelResponse>;
|
|
10
|
+
getResponse(body: GetResponseRequest): Promise<GetResponseResponse>;
|
|
11
|
+
}
|
package/dist/api.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import fetch from "cross-fetch";
|
|
2
|
+
export class YetterImageClient {
|
|
3
|
+
constructor(options) {
|
|
4
|
+
if (!options.apiKey) {
|
|
5
|
+
throw new Error("`apiKey` is required");
|
|
6
|
+
}
|
|
7
|
+
this.apiKey = options.apiKey;
|
|
8
|
+
this.endpoint = options.endpoint || "https://api.yetter.ai";
|
|
9
|
+
}
|
|
10
|
+
getApiEndpoint() {
|
|
11
|
+
return this.endpoint;
|
|
12
|
+
}
|
|
13
|
+
async generateImage(body) {
|
|
14
|
+
const url = `${this.endpoint}/${body.model}`;
|
|
15
|
+
const res = await fetch(url, {
|
|
16
|
+
method: "POST",
|
|
17
|
+
headers: {
|
|
18
|
+
"Content-Type": "application/json",
|
|
19
|
+
Authorization: `${this.apiKey}`,
|
|
20
|
+
},
|
|
21
|
+
body: JSON.stringify(body),
|
|
22
|
+
});
|
|
23
|
+
if (!res.ok) {
|
|
24
|
+
const errorText = await res.text();
|
|
25
|
+
throw new Error(`API error (${res.status}): ${errorText}`);
|
|
26
|
+
}
|
|
27
|
+
return (await res.json());
|
|
28
|
+
}
|
|
29
|
+
async getStatus(body) {
|
|
30
|
+
const url = new URL(body.url);
|
|
31
|
+
if (body.logs) {
|
|
32
|
+
url.searchParams.append('logs', '1');
|
|
33
|
+
}
|
|
34
|
+
const res = await fetch(url.toString(), {
|
|
35
|
+
method: "GET",
|
|
36
|
+
headers: {
|
|
37
|
+
"Content-Type": "application/json",
|
|
38
|
+
Authorization: `${this.apiKey}`,
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
if (!res.ok) {
|
|
42
|
+
const errorText = await res.text();
|
|
43
|
+
throw new Error(`API error (${res.status}): ${errorText}`);
|
|
44
|
+
}
|
|
45
|
+
return (await res.json());
|
|
46
|
+
}
|
|
47
|
+
async cancel(body) {
|
|
48
|
+
const res = await fetch(body.url, {
|
|
49
|
+
method: "PUT",
|
|
50
|
+
headers: {
|
|
51
|
+
"Content-Type": "application/json",
|
|
52
|
+
Authorization: `${this.apiKey}`,
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
if (!res.ok) {
|
|
56
|
+
const errorText = await res.text();
|
|
57
|
+
throw new Error(`API error (${res.status}): ${errorText}`);
|
|
58
|
+
}
|
|
59
|
+
return (await res.json());
|
|
60
|
+
}
|
|
61
|
+
async getResponse(body) {
|
|
62
|
+
const res = await fetch(body.url, {
|
|
63
|
+
method: "GET",
|
|
64
|
+
headers: {
|
|
65
|
+
"Content-Type": "application/json",
|
|
66
|
+
Authorization: `${this.apiKey}`,
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
if (!res.ok) {
|
|
70
|
+
const errorText = await res.text();
|
|
71
|
+
throw new Error(`API error (${res.status}): ${errorText}`);
|
|
72
|
+
}
|
|
73
|
+
return (await res.json());
|
|
74
|
+
}
|
|
75
|
+
}
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { GetResponseResponse, SubscribeOptions, GenerateImageResponse, SubmitQueueOptions, GetResultOptions, GetResultResponse, StatusOptions, StatusResponse, StreamOptions, YetterStream } from "./types.js";
|
|
2
|
+
export declare class yetter {
|
|
3
|
+
static subscribe(model: string, options: SubscribeOptions): Promise<GetResponseResponse>;
|
|
4
|
+
static queue: {
|
|
5
|
+
submit: (model: string, options: SubmitQueueOptions) => Promise<GenerateImageResponse>;
|
|
6
|
+
status: (model: string, options: StatusOptions) => Promise<StatusResponse>;
|
|
7
|
+
result: (model: string, options: GetResultOptions) => Promise<GetResultResponse>;
|
|
8
|
+
};
|
|
9
|
+
static stream(model: string, options: StreamOptions): Promise<YetterStream>;
|
|
10
|
+
}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
var _a;
|
|
2
|
+
import { YetterImageClient } from "./api.js";
|
|
3
|
+
import { EventSourcePolyfill } from 'event-source-polyfill';
|
|
4
|
+
export class yetter {
|
|
5
|
+
static async subscribe(model, options) {
|
|
6
|
+
var _b;
|
|
7
|
+
if (!process.env.YTR_API_KEY) {
|
|
8
|
+
throw new Error("YTR_API_KEY is not set");
|
|
9
|
+
}
|
|
10
|
+
const client = new YetterImageClient({
|
|
11
|
+
apiKey: process.env.YTR_API_KEY,
|
|
12
|
+
});
|
|
13
|
+
const generateResponse = await client.generateImage({
|
|
14
|
+
model: model,
|
|
15
|
+
...options.input,
|
|
16
|
+
});
|
|
17
|
+
let status = generateResponse.status;
|
|
18
|
+
let lastStatusResponse;
|
|
19
|
+
const startTime = Date.now();
|
|
20
|
+
const timeoutMilliseconds = 30 * 60 * 1000; // 30 minutes
|
|
21
|
+
while (status !== "COMPLETED" && status !== "FAILED") {
|
|
22
|
+
if (Date.now() - startTime > timeoutMilliseconds) {
|
|
23
|
+
console.warn(`Subscription timed out after 30 minutes for request ID: ${generateResponse.request_id}. Attempting to cancel.`);
|
|
24
|
+
try {
|
|
25
|
+
await client.cancel({ url: generateResponse.cancel_url });
|
|
26
|
+
console.log(`Cancellation request sent for request ID: ${generateResponse.request_id}.`);
|
|
27
|
+
}
|
|
28
|
+
catch (cancelError) {
|
|
29
|
+
console.error(`Failed to cancel request ID: ${generateResponse.request_id} after timeout:`, cancelError.message || cancelError);
|
|
30
|
+
}
|
|
31
|
+
throw new Error(`Subscription timed out after 30 minutes for request ID: ${generateResponse.request_id}.`);
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
lastStatusResponse = await client.getStatus({
|
|
35
|
+
url: generateResponse.status_url,
|
|
36
|
+
logs: options.logs,
|
|
37
|
+
});
|
|
38
|
+
status = lastStatusResponse.status;
|
|
39
|
+
if (options.onQueueUpdate) {
|
|
40
|
+
options.onQueueUpdate(lastStatusResponse);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
console.error("Error during status polling:", error);
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (status === "FAILED") {
|
|
49
|
+
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.";
|
|
50
|
+
throw new Error(errorMessage);
|
|
51
|
+
}
|
|
52
|
+
const finalResponse = await client.getResponse({
|
|
53
|
+
url: generateResponse.response_url,
|
|
54
|
+
});
|
|
55
|
+
return finalResponse;
|
|
56
|
+
}
|
|
57
|
+
static async stream(model, options) {
|
|
58
|
+
if (!process.env.YTR_API_KEY) {
|
|
59
|
+
throw new Error("YTR_API_KEY is not set");
|
|
60
|
+
}
|
|
61
|
+
const apiClient = new YetterImageClient({
|
|
62
|
+
apiKey: process.env.YTR_API_KEY,
|
|
63
|
+
});
|
|
64
|
+
const initialApiResponse = await apiClient.generateImage({
|
|
65
|
+
model: model,
|
|
66
|
+
...options.input,
|
|
67
|
+
});
|
|
68
|
+
const requestId = initialApiResponse.request_id;
|
|
69
|
+
const responseUrl = initialApiResponse.response_url;
|
|
70
|
+
const cancelUrl = initialApiResponse.cancel_url;
|
|
71
|
+
const sseStreamUrl = `${apiClient.getApiEndpoint()}/${model}/requests/${requestId}/status/stream`;
|
|
72
|
+
let eventSource;
|
|
73
|
+
let streamEnded = false;
|
|
74
|
+
// Setup the promise for the done() method
|
|
75
|
+
let resolveDonePromise;
|
|
76
|
+
let rejectDonePromise;
|
|
77
|
+
const donePromise = new Promise((resolve, reject) => {
|
|
78
|
+
resolveDonePromise = resolve;
|
|
79
|
+
rejectDonePromise = reject;
|
|
80
|
+
});
|
|
81
|
+
const controller = {
|
|
82
|
+
// This will be used by the async iterator to pull events
|
|
83
|
+
// It needs a way to buffer events or signal availability
|
|
84
|
+
events: [],
|
|
85
|
+
resolvers: [],
|
|
86
|
+
isClosed: false,
|
|
87
|
+
push(event) {
|
|
88
|
+
var _b;
|
|
89
|
+
if (this.isClosed)
|
|
90
|
+
return;
|
|
91
|
+
if (this.resolvers.length > 0) {
|
|
92
|
+
this.resolvers.shift()({ value: event, done: false });
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
this.events.push(event);
|
|
96
|
+
}
|
|
97
|
+
// Check for terminal events to resolve/reject the donePromise
|
|
98
|
+
if (event.status === "COMPLETED") {
|
|
99
|
+
streamEnded = true;
|
|
100
|
+
apiClient.getResponse({ url: responseUrl })
|
|
101
|
+
.then(resolveDonePromise)
|
|
102
|
+
.catch(rejectDonePromise)
|
|
103
|
+
.finally(() => this.close());
|
|
104
|
+
}
|
|
105
|
+
else if (event.status === "FAILED") {
|
|
106
|
+
streamEnded = true;
|
|
107
|
+
rejectDonePromise(new Error(((_b = event.logs) === null || _b === void 0 ? void 0 : _b.map(l => l.message).join('\n')) || `Stream reported FAILED for ${requestId}`));
|
|
108
|
+
this.close();
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
error(err) {
|
|
112
|
+
if (this.isClosed)
|
|
113
|
+
return;
|
|
114
|
+
// If streamEnded is true, it means a terminal event (COMPLETED/FAILED)
|
|
115
|
+
// has already been processed and is handling the donePromise.
|
|
116
|
+
// This error is likely a secondary effect of the connection closing.
|
|
117
|
+
if (!streamEnded) {
|
|
118
|
+
rejectDonePromise(err); // Only reject if no terminal event was processed
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
console.warn("SSE 'onerror' event after stream was considered ended (COMPLETED/FAILED). This error will not alter the done() promise.", err);
|
|
122
|
+
}
|
|
123
|
+
streamEnded = true; // Ensure it's marked as ended
|
|
124
|
+
if (this.resolvers.length > 0) {
|
|
125
|
+
this.resolvers.shift()({ value: undefined, done: true }); // Signal error to iterator consumer
|
|
126
|
+
}
|
|
127
|
+
this.close(); // Proceed to close EventSource etc.
|
|
128
|
+
},
|
|
129
|
+
close() {
|
|
130
|
+
if (this.isClosed)
|
|
131
|
+
return;
|
|
132
|
+
this.isClosed = true;
|
|
133
|
+
this.resolvers.forEach(resolve => resolve({ value: undefined, done: true }));
|
|
134
|
+
this.resolvers = [];
|
|
135
|
+
eventSource === null || eventSource === void 0 ? void 0 : eventSource.close();
|
|
136
|
+
// If donePromise is still pending, reject it as stream closed prematurely
|
|
137
|
+
// Use a timeout to allow any final event processing for COMPLETED/FAILED to settle it first
|
|
138
|
+
setTimeout(() => {
|
|
139
|
+
donePromise.catch(() => { }).finally(() => {
|
|
140
|
+
if (!streamEnded) { // If not explicitly completed or failed
|
|
141
|
+
rejectDonePromise(new Error("Stream closed prematurely or unexpectedly."));
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
}, 100);
|
|
145
|
+
},
|
|
146
|
+
next() {
|
|
147
|
+
if (this.events.length > 0) {
|
|
148
|
+
return Promise.resolve({ value: this.events.shift(), done: false });
|
|
149
|
+
}
|
|
150
|
+
if (this.isClosed) {
|
|
151
|
+
return Promise.resolve({ value: undefined, done: true });
|
|
152
|
+
}
|
|
153
|
+
return new Promise(resolve => this.resolvers.push(resolve));
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
eventSource = new EventSourcePolyfill(sseStreamUrl, {
|
|
157
|
+
headers: { 'Authorization': `${process.env.YTR_API_KEY}` }
|
|
158
|
+
});
|
|
159
|
+
eventSource.onopen = (event) => {
|
|
160
|
+
console.log("SSE Connection Opened:", event);
|
|
161
|
+
};
|
|
162
|
+
eventSource.addEventListener('data', (event) => {
|
|
163
|
+
console.log("SSE 'data' event received, raw data:", event.data);
|
|
164
|
+
try {
|
|
165
|
+
const statusData = JSON.parse(event.data);
|
|
166
|
+
controller.push(statusData);
|
|
167
|
+
}
|
|
168
|
+
catch (e) {
|
|
169
|
+
console.error("Error parsing SSE 'data' event:", e, "Raw data:", event.data);
|
|
170
|
+
controller.error(new Error(`Error parsing SSE 'data' event: ${e.message}`));
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
eventSource.addEventListener('done', (event) => {
|
|
174
|
+
console.log("SSE 'done' event received, raw data:", event.data);
|
|
175
|
+
controller.close();
|
|
176
|
+
});
|
|
177
|
+
eventSource.onerror = (err) => {
|
|
178
|
+
console.error("SSE Connection Error (onerror):", err);
|
|
179
|
+
controller.error(err);
|
|
180
|
+
};
|
|
181
|
+
// Handle if API immediately returns a terminal status in initialApiResponse (e.g. already completed/failed)
|
|
182
|
+
if (initialApiResponse.status === "COMPLETED") {
|
|
183
|
+
controller.push(initialApiResponse);
|
|
184
|
+
}
|
|
185
|
+
else if (initialApiResponse.status === "FAILED") {
|
|
186
|
+
controller.push(initialApiResponse);
|
|
187
|
+
}
|
|
188
|
+
return {
|
|
189
|
+
async *[Symbol.asyncIterator]() {
|
|
190
|
+
while (!controller.isClosed || controller.events.length > 0) {
|
|
191
|
+
const event = await controller.next();
|
|
192
|
+
if (event.done)
|
|
193
|
+
break;
|
|
194
|
+
yield event.value;
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
done: () => donePromise,
|
|
198
|
+
cancel: async () => {
|
|
199
|
+
controller.close();
|
|
200
|
+
try {
|
|
201
|
+
await apiClient.cancel({ url: cancelUrl });
|
|
202
|
+
console.log(`Stream for ${requestId} - underlying request cancelled.`);
|
|
203
|
+
}
|
|
204
|
+
catch (e) {
|
|
205
|
+
console.error(`Error cancelling underlying request for stream ${requestId}:`, e.message);
|
|
206
|
+
}
|
|
207
|
+
// Ensure donePromise is settled if not already
|
|
208
|
+
if (!streamEnded) {
|
|
209
|
+
rejectDonePromise(new Error("Stream was cancelled by user."));
|
|
210
|
+
}
|
|
211
|
+
},
|
|
212
|
+
getRequestId: () => requestId,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
_a = yetter;
|
|
217
|
+
yetter.queue = {
|
|
218
|
+
submit: async (model, options) => {
|
|
219
|
+
if (!process.env.YTR_API_KEY) {
|
|
220
|
+
throw new Error("YTR_API_KEY is not set");
|
|
221
|
+
}
|
|
222
|
+
const client = new YetterImageClient({
|
|
223
|
+
apiKey: process.env.YTR_API_KEY,
|
|
224
|
+
});
|
|
225
|
+
const generateResponse = await client.generateImage({
|
|
226
|
+
model: model,
|
|
227
|
+
...options.input,
|
|
228
|
+
});
|
|
229
|
+
return generateResponse;
|
|
230
|
+
},
|
|
231
|
+
status: async (model, options) => {
|
|
232
|
+
if (!process.env.YTR_API_KEY) {
|
|
233
|
+
throw new Error("YTR_API_KEY is not set");
|
|
234
|
+
}
|
|
235
|
+
const client = new YetterImageClient({
|
|
236
|
+
apiKey: process.env.YTR_API_KEY,
|
|
237
|
+
});
|
|
238
|
+
const endpoint = client.getApiEndpoint();
|
|
239
|
+
const statusUrl = `${endpoint}/${model}/requests/${options.requestId}/status`;
|
|
240
|
+
const statusData = await client.getStatus({ url: statusUrl });
|
|
241
|
+
return {
|
|
242
|
+
data: statusData,
|
|
243
|
+
requestId: options.requestId,
|
|
244
|
+
};
|
|
245
|
+
},
|
|
246
|
+
result: async (model, options) => {
|
|
247
|
+
if (!process.env.YTR_API_KEY) {
|
|
248
|
+
throw new Error("YTR_API_KEY is not set");
|
|
249
|
+
}
|
|
250
|
+
const client = new YetterImageClient({
|
|
251
|
+
apiKey: process.env.YTR_API_KEY,
|
|
252
|
+
});
|
|
253
|
+
const endpoint = client.getApiEndpoint();
|
|
254
|
+
const responseUrl = `${endpoint}/${model}/requests/${options.requestId}`;
|
|
255
|
+
const responseData = await client.getResponse({ url: responseUrl });
|
|
256
|
+
return {
|
|
257
|
+
data: responseData,
|
|
258
|
+
requestId: options.requestId,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
};
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
export interface ClientOptions {
|
|
2
|
+
apiKey: string;
|
|
3
|
+
endpoint?: string;
|
|
4
|
+
}
|
|
5
|
+
export interface GenerateImageRequest {
|
|
6
|
+
model: string;
|
|
7
|
+
prompt: string;
|
|
8
|
+
num_inference_steps?: number;
|
|
9
|
+
seed?: number;
|
|
10
|
+
}
|
|
11
|
+
export interface GenerateImageResponse {
|
|
12
|
+
status: string;
|
|
13
|
+
request_id: string;
|
|
14
|
+
response_url: string;
|
|
15
|
+
status_url: string;
|
|
16
|
+
cancel_url: string;
|
|
17
|
+
queue_position: number;
|
|
18
|
+
}
|
|
19
|
+
export interface LogEntry {
|
|
20
|
+
message: string;
|
|
21
|
+
}
|
|
22
|
+
export interface GetStatusRequest {
|
|
23
|
+
url: string;
|
|
24
|
+
logs?: boolean;
|
|
25
|
+
}
|
|
26
|
+
export interface GetStatusResponse {
|
|
27
|
+
status: string;
|
|
28
|
+
request_id?: string;
|
|
29
|
+
response_url?: string;
|
|
30
|
+
status_url?: string;
|
|
31
|
+
cancel_url?: string;
|
|
32
|
+
queue_position?: number;
|
|
33
|
+
logs?: LogEntry[];
|
|
34
|
+
}
|
|
35
|
+
export interface CancelRequest {
|
|
36
|
+
url: string;
|
|
37
|
+
}
|
|
38
|
+
export interface CancelResponse {
|
|
39
|
+
status: string;
|
|
40
|
+
request_id?: string;
|
|
41
|
+
response_url?: string;
|
|
42
|
+
status_url?: string;
|
|
43
|
+
cancel_url?: string;
|
|
44
|
+
queue_position?: number;
|
|
45
|
+
logs?: string[];
|
|
46
|
+
}
|
|
47
|
+
export interface GetResponseRequest {
|
|
48
|
+
url: string;
|
|
49
|
+
}
|
|
50
|
+
export interface GetResponseResponse {
|
|
51
|
+
images: string[];
|
|
52
|
+
prompt: string;
|
|
53
|
+
}
|
|
54
|
+
export interface SubscribeOptions {
|
|
55
|
+
input: Omit<GenerateImageRequest, 'model'>;
|
|
56
|
+
logs?: boolean;
|
|
57
|
+
onQueueUpdate?: (update: GetStatusResponse) => void;
|
|
58
|
+
}
|
|
59
|
+
export interface SubmitQueueOptions {
|
|
60
|
+
input: Omit<GenerateImageRequest, 'model'>;
|
|
61
|
+
}
|
|
62
|
+
export interface GetResultOptions {
|
|
63
|
+
requestId: string;
|
|
64
|
+
}
|
|
65
|
+
export interface GetResultResponse {
|
|
66
|
+
data: GetResponseResponse;
|
|
67
|
+
requestId: string;
|
|
68
|
+
}
|
|
69
|
+
export interface StatusOptions {
|
|
70
|
+
requestId: string;
|
|
71
|
+
}
|
|
72
|
+
export interface StatusResponse {
|
|
73
|
+
data: GetStatusResponse;
|
|
74
|
+
requestId: string;
|
|
75
|
+
}
|
|
76
|
+
export interface StreamOptions {
|
|
77
|
+
input: Omit<GenerateImageRequest, 'model'>;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Represents an event received from the SSE stream.
|
|
81
|
+
* The `data` field is expected to be compatible with GetStatusResponse.
|
|
82
|
+
*/
|
|
83
|
+
export interface StreamEvent extends GetStatusResponse {
|
|
84
|
+
}
|
|
85
|
+
export interface YetterStream extends AsyncIterable<StreamEvent> {
|
|
86
|
+
done(): Promise<GetResponseResponse>;
|
|
87
|
+
cancel(): Promise<void>;
|
|
88
|
+
getRequestId(): string;
|
|
89
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { yetter } from "../src/client.js";
|
|
2
|
+
|
|
3
|
+
async function main() {
|
|
4
|
+
const model = "ytr-ai/flux/dev";
|
|
5
|
+
console.log("\n--- Starting Stream Test ---");
|
|
6
|
+
let streamRequestId = "";
|
|
7
|
+
|
|
8
|
+
try {
|
|
9
|
+
const streamInstance = await yetter.stream(model, {
|
|
10
|
+
input: {
|
|
11
|
+
prompt: "a bioluminescent forest at night, fantasy art",
|
|
12
|
+
// You can add other parameters like seed or num_inference_steps here
|
|
13
|
+
},
|
|
14
|
+
});
|
|
15
|
+
streamRequestId = streamInstance.getRequestId();
|
|
16
|
+
console.log(`[${new Date().toLocaleTimeString()}] Stream initiated for Request ID: ${streamRequestId}`);
|
|
17
|
+
|
|
18
|
+
// Iterate over stream events
|
|
19
|
+
for await (const event of streamInstance) {
|
|
20
|
+
console.log(`[${new Date().toLocaleTimeString()}][STREAM EVENT - ${streamRequestId}] Status: ${event.status}, QPos: ${event.queue_position}`);
|
|
21
|
+
if (event.logs && event.logs.length > 0) {
|
|
22
|
+
console.log(` Logs for ${streamRequestId}:`);
|
|
23
|
+
event.logs.forEach(log => console.log(` - ${log.message}`));
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
console.log(`[${new Date().toLocaleTimeString()}] Stream for ${streamRequestId} finished iterating events.`);
|
|
27
|
+
|
|
28
|
+
// Wait for the final result from the done() method
|
|
29
|
+
console.log(`[${new Date().toLocaleTimeString()}] Waiting for done() on stream for ${streamRequestId}...`);
|
|
30
|
+
const result = await streamInstance.done();
|
|
31
|
+
console.log("\n--- Stream Test Final Result ---");
|
|
32
|
+
console.log(`Request ID: ${streamRequestId}`);
|
|
33
|
+
if (result.images && result.images.length > 0) {
|
|
34
|
+
console.log("Generated Images:", result.images);
|
|
35
|
+
} else {
|
|
36
|
+
console.log("No images in final result.");
|
|
37
|
+
}
|
|
38
|
+
console.log("Original Prompt:", result.prompt);
|
|
39
|
+
|
|
40
|
+
} catch (err: any) {
|
|
41
|
+
console.error(`\n--- Stream Test Failed (Request ID: ${streamRequestId || 'UNKNOWN'}) ---`);
|
|
42
|
+
console.error("Error during stream test:", err.message || err);
|
|
43
|
+
if (err.stack) {
|
|
44
|
+
console.error(err.stack);
|
|
45
|
+
}
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
main().catch(err => {
|
|
51
|
+
console.error("Unhandled error in main:", err);
|
|
52
|
+
process.exit(1);
|
|
53
|
+
});
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { yetter } from "../src/client.js";
|
|
2
|
+
|
|
3
|
+
async function main() {
|
|
4
|
+
const model = "ytr-ai/flux/dev";
|
|
5
|
+
// Queue Submit test
|
|
6
|
+
try {
|
|
7
|
+
console.log("\n--- Starting Queue Submit Test ---");
|
|
8
|
+
const { request_id, status, queue_position } = await yetter.queue.submit(model, {
|
|
9
|
+
input: {
|
|
10
|
+
prompt: "a fluffy white kitten playing with a yarn ball",
|
|
11
|
+
},
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
console.log("\n--- Queue Submit Test Result ---");
|
|
15
|
+
console.log("Request ID:", request_id);
|
|
16
|
+
console.log("Status:", status);
|
|
17
|
+
console.log("Queue Position:", queue_position);
|
|
18
|
+
|
|
19
|
+
if (request_id) {
|
|
20
|
+
console.log(`\n--- Polling for status of Request ID: ${request_id} (3-minute timeout) ---`);
|
|
21
|
+
|
|
22
|
+
const startTime = Date.now();
|
|
23
|
+
const timeoutMilliseconds = 3 * 60 * 1000; // 3 minutes
|
|
24
|
+
const pollIntervalMilliseconds = 10 * 1000; // Poll every 10 seconds
|
|
25
|
+
let success = false;
|
|
26
|
+
|
|
27
|
+
while (Date.now() - startTime < timeoutMilliseconds) {
|
|
28
|
+
try {
|
|
29
|
+
const statusResult = await yetter.queue.status(model, {
|
|
30
|
+
requestId: request_id,
|
|
31
|
+
});
|
|
32
|
+
const currentStatus = statusResult.data.status;
|
|
33
|
+
console.log(`[${new Date().toLocaleTimeString()}] Request ${request_id} status: ${currentStatus}, Queue Position: ${statusResult.data.queue_position}`);
|
|
34
|
+
|
|
35
|
+
if (currentStatus === "COMPLETED") {
|
|
36
|
+
console.log("Status is COMPLETED. Fetching final result...");
|
|
37
|
+
const finalResult = await yetter.queue.result(model, {
|
|
38
|
+
requestId: request_id,
|
|
39
|
+
});
|
|
40
|
+
console.log("\n--- Get Result Test Succeeded ---");
|
|
41
|
+
console.log("Request ID:", finalResult.requestId);
|
|
42
|
+
console.log("Image Data:", finalResult.data.images);
|
|
43
|
+
console.log("Prompt:", finalResult.data.prompt);
|
|
44
|
+
success = true;
|
|
45
|
+
break; // Exit loop on success
|
|
46
|
+
} else if (currentStatus === "FAILED") {
|
|
47
|
+
console.error(`Request ${request_id} FAILED. Logs:`, statusResult.data.logs);
|
|
48
|
+
break; // Exit loop on failure
|
|
49
|
+
}
|
|
50
|
+
// For other statuses (e.g., IN_QUEUE, IN_PROGRESS), continue polling
|
|
51
|
+
|
|
52
|
+
} catch (pollError: any) {
|
|
53
|
+
console.error(`Error during status polling for ${request_id} (will retry):`, pollError.message || pollError);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (Date.now() - startTime + pollIntervalMilliseconds >= timeoutMilliseconds) {
|
|
57
|
+
if (!success) console.error("Timeout approaching, last poll cycle completed or error occurred.");
|
|
58
|
+
break; // Don't wait if the next interval exceeds timeout
|
|
59
|
+
}
|
|
60
|
+
await new Promise(resolve => setTimeout(resolve, pollIntervalMilliseconds));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (!success) {
|
|
64
|
+
console.error(`\n--- Polling Timed Out or Failed for Request ID: ${request_id} ---`);
|
|
65
|
+
console.error(`Failed to get a COMPLETED status for ${request_id} within 3 minutes.`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
} catch (err: any) {
|
|
70
|
+
console.error("\n--- Queue Submit Test Failed ---");
|
|
71
|
+
console.error("Error during queue submit:", err.message || err);
|
|
72
|
+
if (err.stack) {
|
|
73
|
+
console.error(err.stack);
|
|
74
|
+
}
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
main();
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { yetter } from "../dist/index.js";
|
|
2
|
+
|
|
3
|
+
async function main() {
|
|
4
|
+
const model = "ytr-ai/flux/dev";
|
|
5
|
+
// Subscribe test
|
|
6
|
+
try {
|
|
7
|
+
console.log("\n--- Starting Subscribe Test ---");
|
|
8
|
+
const result = await yetter.subscribe(model, {
|
|
9
|
+
input: {
|
|
10
|
+
prompt: "a vibrant coral reef, underwater photography",
|
|
11
|
+
},
|
|
12
|
+
logs: true,
|
|
13
|
+
onQueueUpdate: (update) => {
|
|
14
|
+
console.log(`[Queue Update] Status: ${update.status}, Position: ${update.queue_position}`);
|
|
15
|
+
if (update.status === "IN_PROGRESS" && update.logs) {
|
|
16
|
+
console.log("Logs:");
|
|
17
|
+
update.logs.map((log) => log.message).forEach(logMessage => console.log(` - ${logMessage}`));
|
|
18
|
+
} else if (update.status === "COMPLETED") {
|
|
19
|
+
console.log("Processing completed!");
|
|
20
|
+
} else if (update.status === "FAILED") {
|
|
21
|
+
console.error("Processing failed. Logs:", update.logs);
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
console.log("\n--- Subscribe Test Result ---");
|
|
27
|
+
console.log("Results:", result);
|
|
28
|
+
|
|
29
|
+
} catch (err: any) {
|
|
30
|
+
console.error("\n--- Subscribe Test Failed ---");
|
|
31
|
+
console.error("Error during subscribe:", err.message || err);
|
|
32
|
+
if (err.stack) {
|
|
33
|
+
console.error(err.stack);
|
|
34
|
+
}
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
main();
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@yetter/client",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"build": "tsc",
|
|
7
|
+
"test:submit": "node --loader ts-node/esm examples/submit.ts",
|
|
8
|
+
"test:subscribe": "node --loader ts-node/esm examples/subscribe.ts",
|
|
9
|
+
"test:stream": "node --loader ts-node/esm examples/stream.ts",
|
|
10
|
+
"prepublishOnly": "npm run build"
|
|
11
|
+
},
|
|
12
|
+
"main": "dist/index.js",
|
|
13
|
+
"types": "dist/index.d.ts",
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"import": "./dist/index.js",
|
|
17
|
+
"types": "./dist/index.d.ts"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"keywords": [],
|
|
21
|
+
"author": "",
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"description": "",
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@types/eventsource": "^1.1.15",
|
|
26
|
+
"cross-fetch": "^4.1.0",
|
|
27
|
+
"event-source-polyfill": "^1.0.31",
|
|
28
|
+
"eventsource": "^4.0.0"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/event-source-polyfill": "^1.0.5",
|
|
32
|
+
"@types/node": "^22.15.23",
|
|
33
|
+
"eslint": "^9.27.0",
|
|
34
|
+
"prettier": "^3.5.3",
|
|
35
|
+
"ts-lib": "^0.0.5",
|
|
36
|
+
"ts-node": "^10.9.2",
|
|
37
|
+
"typescript": "^5.8.3"
|
|
38
|
+
}
|
|
39
|
+
}
|
package/src/api.ts
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import fetch from "cross-fetch";
|
|
2
|
+
import {
|
|
3
|
+
ClientOptions,
|
|
4
|
+
GenerateImageRequest,
|
|
5
|
+
GenerateImageResponse,
|
|
6
|
+
GetStatusRequest,
|
|
7
|
+
GetStatusResponse,
|
|
8
|
+
CancelRequest,
|
|
9
|
+
CancelResponse,
|
|
10
|
+
GetResponseRequest,
|
|
11
|
+
GetResponseResponse,
|
|
12
|
+
} from "./types";
|
|
13
|
+
|
|
14
|
+
export class YetterImageClient {
|
|
15
|
+
private apiKey: string;
|
|
16
|
+
private endpoint: string;
|
|
17
|
+
|
|
18
|
+
constructor(options: ClientOptions) {
|
|
19
|
+
if (!options.apiKey) {
|
|
20
|
+
throw new Error("`apiKey` is required");
|
|
21
|
+
}
|
|
22
|
+
this.apiKey = options.apiKey;
|
|
23
|
+
this.endpoint = options.endpoint || "https://api.yetter.ai";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
public getApiEndpoint(): string {
|
|
27
|
+
return this.endpoint;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
public async generateImage(
|
|
31
|
+
body: GenerateImageRequest
|
|
32
|
+
): Promise<GenerateImageResponse> {
|
|
33
|
+
const url = `${this.endpoint}/${body.model}`;
|
|
34
|
+
const res = await fetch(url, {
|
|
35
|
+
method: "POST",
|
|
36
|
+
headers: {
|
|
37
|
+
"Content-Type": "application/json",
|
|
38
|
+
Authorization: `${this.apiKey}`,
|
|
39
|
+
},
|
|
40
|
+
body: JSON.stringify(body),
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
if (!res.ok) {
|
|
44
|
+
const errorText = await res.text();
|
|
45
|
+
throw new Error(`API error (${res.status}): ${errorText}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return (await res.json()) as GenerateImageResponse;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
public async getStatus(body: GetStatusRequest): Promise<GetStatusResponse> {
|
|
52
|
+
const url = new URL(body.url);
|
|
53
|
+
if (body.logs) {
|
|
54
|
+
url.searchParams.append('logs', '1');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const res = await fetch(url.toString(), {
|
|
58
|
+
method: "GET",
|
|
59
|
+
headers: {
|
|
60
|
+
"Content-Type": "application/json",
|
|
61
|
+
Authorization: `${this.apiKey}`,
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
if (!res.ok) {
|
|
66
|
+
const errorText = await res.text();
|
|
67
|
+
throw new Error(`API error (${res.status}): ${errorText}`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return (await res.json()) as GetStatusResponse;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
public async cancel(body: CancelRequest): Promise<CancelResponse> {
|
|
74
|
+
const res = await fetch(body.url, {
|
|
75
|
+
method: "PUT",
|
|
76
|
+
headers: {
|
|
77
|
+
"Content-Type": "application/json",
|
|
78
|
+
Authorization: `${this.apiKey}`,
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
if (!res.ok) {
|
|
83
|
+
const errorText = await res.text();
|
|
84
|
+
throw new Error(`API error (${res.status}): ${errorText}`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return (await res.json()) as CancelResponse;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
public async getResponse(body: GetResponseRequest): Promise<GetResponseResponse> {
|
|
91
|
+
const res = await fetch(body.url, {
|
|
92
|
+
method: "GET",
|
|
93
|
+
headers: {
|
|
94
|
+
"Content-Type": "application/json",
|
|
95
|
+
Authorization: `${this.apiKey}`,
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
if (!res.ok) {
|
|
100
|
+
const errorText = await res.text();
|
|
101
|
+
throw new Error(`API error (${res.status}): ${errorText}`);
|
|
102
|
+
}
|
|
103
|
+
return (await res.json()) as GetResponseResponse;
|
|
104
|
+
}
|
|
105
|
+
}
|
package/src/client.ts
ADDED
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import { YetterImageClient } from "./api.js";
|
|
2
|
+
import { EventSourcePolyfill } from 'event-source-polyfill';
|
|
3
|
+
import {
|
|
4
|
+
GetStatusResponse,
|
|
5
|
+
GetResponseResponse,
|
|
6
|
+
SubscribeOptions,
|
|
7
|
+
GenerateImageResponse,
|
|
8
|
+
SubmitQueueOptions,
|
|
9
|
+
GetResultOptions,
|
|
10
|
+
GetResultResponse,
|
|
11
|
+
StatusOptions,
|
|
12
|
+
StatusResponse,
|
|
13
|
+
StreamOptions,
|
|
14
|
+
StreamEvent,
|
|
15
|
+
YetterStream,
|
|
16
|
+
} from "./types.js";
|
|
17
|
+
|
|
18
|
+
export class yetter {
|
|
19
|
+
public static async subscribe(
|
|
20
|
+
model: string,
|
|
21
|
+
options: SubscribeOptions
|
|
22
|
+
): Promise<GetResponseResponse> {
|
|
23
|
+
if (!process.env.YTR_API_KEY) {
|
|
24
|
+
throw new Error("YTR_API_KEY is not set");
|
|
25
|
+
}
|
|
26
|
+
const client = new YetterImageClient({
|
|
27
|
+
apiKey: process.env.YTR_API_KEY,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const generateResponse = await client.generateImage({
|
|
31
|
+
model: model,
|
|
32
|
+
...options.input,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
let status = generateResponse.status;
|
|
36
|
+
let lastStatusResponse: GetStatusResponse | undefined;
|
|
37
|
+
|
|
38
|
+
const startTime = Date.now();
|
|
39
|
+
const timeoutMilliseconds = 30 * 60 * 1000; // 30 minutes
|
|
40
|
+
|
|
41
|
+
while (status !== "COMPLETED" && status !== "FAILED") {
|
|
42
|
+
if (Date.now() - startTime > timeoutMilliseconds) {
|
|
43
|
+
console.warn(`Subscription timed out after 30 minutes for request ID: ${generateResponse.request_id}. Attempting to cancel.`);
|
|
44
|
+
try {
|
|
45
|
+
await client.cancel({ url: generateResponse.cancel_url });
|
|
46
|
+
console.log(`Cancellation request sent for request ID: ${generateResponse.request_id}.`);
|
|
47
|
+
} catch (cancelError: any) {
|
|
48
|
+
console.error(`Failed to cancel request ID: ${generateResponse.request_id} after timeout:`, cancelError.message || cancelError);
|
|
49
|
+
}
|
|
50
|
+
throw new Error(`Subscription timed out after 30 minutes for request ID: ${generateResponse.request_id}.`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
lastStatusResponse = await client.getStatus({
|
|
55
|
+
url: generateResponse.status_url,
|
|
56
|
+
logs: options.logs,
|
|
57
|
+
});
|
|
58
|
+
status = lastStatusResponse.status;
|
|
59
|
+
|
|
60
|
+
if (options.onQueueUpdate) {
|
|
61
|
+
options.onQueueUpdate(lastStatusResponse);
|
|
62
|
+
}
|
|
63
|
+
} catch (error) {
|
|
64
|
+
console.error("Error during status polling:", error);
|
|
65
|
+
throw error;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (status === "FAILED") {
|
|
70
|
+
const errorMessage = lastStatusResponse?.logs?.map(log => log.message).join("\n") || "Image generation failed.";
|
|
71
|
+
throw new Error(errorMessage);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const finalResponse = await client.getResponse({
|
|
75
|
+
url: generateResponse.response_url,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
return finalResponse;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
public static queue = {
|
|
82
|
+
submit: async (
|
|
83
|
+
model: string,
|
|
84
|
+
options: SubmitQueueOptions
|
|
85
|
+
): Promise<GenerateImageResponse> => {
|
|
86
|
+
if (!process.env.YTR_API_KEY) {
|
|
87
|
+
throw new Error("YTR_API_KEY is not set");
|
|
88
|
+
}
|
|
89
|
+
const client = new YetterImageClient({
|
|
90
|
+
apiKey: process.env.YTR_API_KEY,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const generateResponse = await client.generateImage({
|
|
94
|
+
model: model,
|
|
95
|
+
...options.input,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
return generateResponse;
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
status: async (
|
|
102
|
+
model: string,
|
|
103
|
+
options: StatusOptions
|
|
104
|
+
): Promise<StatusResponse> => {
|
|
105
|
+
if (!process.env.YTR_API_KEY) {
|
|
106
|
+
throw new Error("YTR_API_KEY is not set");
|
|
107
|
+
}
|
|
108
|
+
const client = new YetterImageClient({
|
|
109
|
+
apiKey: process.env.YTR_API_KEY,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
const endpoint = client.getApiEndpoint();
|
|
113
|
+
|
|
114
|
+
const statusUrl = `${endpoint}/${model}/requests/${options.requestId}/status`;
|
|
115
|
+
|
|
116
|
+
const statusData = await client.getStatus({ url: statusUrl });
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
data: statusData,
|
|
120
|
+
requestId: options.requestId,
|
|
121
|
+
};
|
|
122
|
+
},
|
|
123
|
+
|
|
124
|
+
result: async (
|
|
125
|
+
model: string,
|
|
126
|
+
options: GetResultOptions
|
|
127
|
+
): Promise<GetResultResponse> => {
|
|
128
|
+
if (!process.env.YTR_API_KEY) {
|
|
129
|
+
throw new Error("YTR_API_KEY is not set");
|
|
130
|
+
}
|
|
131
|
+
const client = new YetterImageClient({
|
|
132
|
+
apiKey: process.env.YTR_API_KEY,
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
const endpoint = client.getApiEndpoint();
|
|
136
|
+
|
|
137
|
+
const responseUrl = `${endpoint}/${model}/requests/${options.requestId}`;
|
|
138
|
+
|
|
139
|
+
const responseData = await client.getResponse({ url: responseUrl });
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
data: responseData,
|
|
143
|
+
requestId: options.requestId,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
public static async stream(
|
|
149
|
+
model: string,
|
|
150
|
+
options: StreamOptions
|
|
151
|
+
): Promise<YetterStream> {
|
|
152
|
+
if (!process.env.YTR_API_KEY) {
|
|
153
|
+
throw new Error("YTR_API_KEY is not set");
|
|
154
|
+
}
|
|
155
|
+
const apiClient = new YetterImageClient({
|
|
156
|
+
apiKey: process.env.YTR_API_KEY,
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
const initialApiResponse = await apiClient.generateImage({
|
|
160
|
+
model: model,
|
|
161
|
+
...options.input,
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
const requestId = initialApiResponse.request_id;
|
|
165
|
+
const responseUrl = initialApiResponse.response_url;
|
|
166
|
+
const cancelUrl = initialApiResponse.cancel_url;
|
|
167
|
+
const sseStreamUrl = `${apiClient.getApiEndpoint()}/${model}/requests/${requestId}/status/stream`;
|
|
168
|
+
|
|
169
|
+
let eventSource: EventSource;
|
|
170
|
+
let streamEnded = false;
|
|
171
|
+
|
|
172
|
+
// Setup the promise for the done() method
|
|
173
|
+
let resolveDonePromise: (value: GetResponseResponse) => void;
|
|
174
|
+
let rejectDonePromise: (reason?: any) => void;
|
|
175
|
+
const donePromise = new Promise<GetResponseResponse>((resolve, reject) => {
|
|
176
|
+
resolveDonePromise = resolve;
|
|
177
|
+
rejectDonePromise = reject;
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
const controller = {
|
|
181
|
+
// This will be used by the async iterator to pull events
|
|
182
|
+
// It needs a way to buffer events or signal availability
|
|
183
|
+
events: [] as StreamEvent[],
|
|
184
|
+
resolvers: [] as Array<(value: IteratorResult<StreamEvent, any>) => void>,
|
|
185
|
+
isClosed: false,
|
|
186
|
+
push(event: StreamEvent) {
|
|
187
|
+
if (this.isClosed) return;
|
|
188
|
+
if (this.resolvers.length > 0) {
|
|
189
|
+
this.resolvers.shift()!({ value: event, done: false });
|
|
190
|
+
} else {
|
|
191
|
+
this.events.push(event);
|
|
192
|
+
}
|
|
193
|
+
// Check for terminal events to resolve/reject the donePromise
|
|
194
|
+
if (event.status === "COMPLETED") {
|
|
195
|
+
streamEnded = true;
|
|
196
|
+
apiClient.getResponse({ url: responseUrl })
|
|
197
|
+
.then(resolveDonePromise)
|
|
198
|
+
.catch(rejectDonePromise)
|
|
199
|
+
.finally(() => this.close());
|
|
200
|
+
} else if (event.status === "FAILED") {
|
|
201
|
+
streamEnded = true;
|
|
202
|
+
rejectDonePromise(new Error(event.logs?.map(l => l.message).join('\n') || `Stream reported FAILED for ${requestId}`));
|
|
203
|
+
this.close();
|
|
204
|
+
}
|
|
205
|
+
},
|
|
206
|
+
error(err: any) {
|
|
207
|
+
if (this.isClosed) return;
|
|
208
|
+
|
|
209
|
+
// If streamEnded is true, it means a terminal event (COMPLETED/FAILED)
|
|
210
|
+
// has already been processed and is handling the donePromise.
|
|
211
|
+
// This error is likely a secondary effect of the connection closing.
|
|
212
|
+
if (!streamEnded) {
|
|
213
|
+
rejectDonePromise(err); // Only reject if no terminal event was processed
|
|
214
|
+
} else {
|
|
215
|
+
console.warn("SSE 'onerror' event after stream was considered ended (COMPLETED/FAILED). This error will not alter the done() promise.", err);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
streamEnded = true; // Ensure it's marked as ended
|
|
219
|
+
if (this.resolvers.length > 0) {
|
|
220
|
+
this.resolvers.shift()!({ value: undefined, done: true }); // Signal error to iterator consumer
|
|
221
|
+
}
|
|
222
|
+
this.close(); // Proceed to close EventSource etc.
|
|
223
|
+
},
|
|
224
|
+
close() {
|
|
225
|
+
if (this.isClosed) return;
|
|
226
|
+
this.isClosed = true;
|
|
227
|
+
this.resolvers.forEach(resolve => resolve({ value: undefined, done: true }));
|
|
228
|
+
this.resolvers = [];
|
|
229
|
+
eventSource?.close();
|
|
230
|
+
// If donePromise is still pending, reject it as stream closed prematurely
|
|
231
|
+
// Use a timeout to allow any final event processing for COMPLETED/FAILED to settle it first
|
|
232
|
+
setTimeout(() => {
|
|
233
|
+
donePromise.catch(() => {}).finally(() => { // check if it's already settled
|
|
234
|
+
if (!streamEnded) { // If not explicitly completed or failed
|
|
235
|
+
rejectDonePromise(new Error("Stream closed prematurely or unexpectedly."));
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
}, 100);
|
|
239
|
+
},
|
|
240
|
+
next(): Promise<IteratorResult<StreamEvent, any>> {
|
|
241
|
+
if (this.events.length > 0) {
|
|
242
|
+
return Promise.resolve({ value: this.events.shift()!, done: false });
|
|
243
|
+
}
|
|
244
|
+
if (this.isClosed) {
|
|
245
|
+
return Promise.resolve({ value: undefined, done: true });
|
|
246
|
+
}
|
|
247
|
+
return new Promise(resolve => this.resolvers.push(resolve));
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
eventSource = new EventSourcePolyfill(sseStreamUrl, {
|
|
252
|
+
headers: { 'Authorization': `${process.env.YTR_API_KEY}` }
|
|
253
|
+
} as any);
|
|
254
|
+
|
|
255
|
+
eventSource.onopen = (event: Event) => {
|
|
256
|
+
console.log("SSE Connection Opened:", event);
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
eventSource.addEventListener('data', (event: MessageEvent) => {
|
|
260
|
+
console.log("SSE 'data' event received, raw data:", event.data);
|
|
261
|
+
try {
|
|
262
|
+
const statusData: GetStatusResponse = JSON.parse(event.data as string);
|
|
263
|
+
controller.push(statusData as StreamEvent);
|
|
264
|
+
} catch (e: any) {
|
|
265
|
+
console.error("Error parsing SSE 'data' event:", e, "Raw data:", event.data);
|
|
266
|
+
controller.error(new Error(`Error parsing SSE 'data' event: ${e.message}`));
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
eventSource.addEventListener('done', (event: MessageEvent) => {
|
|
271
|
+
console.log("SSE 'done' event received, raw data:", event.data);
|
|
272
|
+
controller.close();
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
eventSource.onerror = (err: Event | MessageEvent) => {
|
|
276
|
+
console.error("SSE Connection Error (onerror):", err);
|
|
277
|
+
controller.error(err);
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
// Handle if API immediately returns a terminal status in initialApiResponse (e.g. already completed/failed)
|
|
281
|
+
if (initialApiResponse.status === "COMPLETED"){
|
|
282
|
+
controller.push(initialApiResponse as any as StreamEvent);
|
|
283
|
+
} else if (initialApiResponse.status === "FAILED"){
|
|
284
|
+
controller.push(initialApiResponse as any as StreamEvent);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return {
|
|
288
|
+
async *[Symbol.asyncIterator]() {
|
|
289
|
+
while (!controller.isClosed || controller.events.length > 0) {
|
|
290
|
+
const event = await controller.next();
|
|
291
|
+
if (event.done) break;
|
|
292
|
+
yield event.value;
|
|
293
|
+
}
|
|
294
|
+
},
|
|
295
|
+
done: () => donePromise,
|
|
296
|
+
cancel: async () => {
|
|
297
|
+
controller.close();
|
|
298
|
+
try {
|
|
299
|
+
await apiClient.cancel({ url: cancelUrl });
|
|
300
|
+
console.log(`Stream for ${requestId} - underlying request cancelled.`);
|
|
301
|
+
} catch (e: any) {
|
|
302
|
+
console.error(`Error cancelling underlying request for stream ${requestId}:`, e.message);
|
|
303
|
+
}
|
|
304
|
+
// Ensure donePromise is settled if not already
|
|
305
|
+
if (!streamEnded) {
|
|
306
|
+
rejectDonePromise(new Error("Stream was cancelled by user."));
|
|
307
|
+
}
|
|
308
|
+
},
|
|
309
|
+
getRequestId: () => requestId,
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
}
|
package/src/index.ts
ADDED
package/src/types.ts
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
export interface ClientOptions {
|
|
2
|
+
apiKey: string;
|
|
3
|
+
endpoint?: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface GenerateImageRequest {
|
|
7
|
+
model: string;
|
|
8
|
+
prompt: string;
|
|
9
|
+
num_inference_steps?: number;
|
|
10
|
+
seed?: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface GenerateImageResponse {
|
|
14
|
+
status: string;
|
|
15
|
+
request_id: string;
|
|
16
|
+
response_url: string;
|
|
17
|
+
status_url: string;
|
|
18
|
+
cancel_url: string;
|
|
19
|
+
queue_position: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface LogEntry {
|
|
23
|
+
message: string;
|
|
24
|
+
// Add other potential log fields here, e.g., timestamp, level
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface GetStatusRequest {
|
|
28
|
+
url: string;
|
|
29
|
+
logs?: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface GetStatusResponse {
|
|
33
|
+
status: string;
|
|
34
|
+
request_id?: string;
|
|
35
|
+
response_url?: string;
|
|
36
|
+
status_url?: string;
|
|
37
|
+
cancel_url?: string;
|
|
38
|
+
queue_position?: number;
|
|
39
|
+
logs?: LogEntry[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface CancelRequest {
|
|
43
|
+
url: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface CancelResponse {
|
|
47
|
+
status: string;
|
|
48
|
+
request_id?: string;
|
|
49
|
+
response_url?: string;
|
|
50
|
+
status_url?: string;
|
|
51
|
+
cancel_url?: string;
|
|
52
|
+
queue_position?: number;
|
|
53
|
+
logs?: string[];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface GetResponseRequest {
|
|
57
|
+
url: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface GetResponseResponse {
|
|
61
|
+
images: string[];
|
|
62
|
+
prompt: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface SubscribeOptions {
|
|
66
|
+
input: Omit<GenerateImageRequest, 'model'>; // model is a direct param to subscribe
|
|
67
|
+
logs?: boolean;
|
|
68
|
+
onQueueUpdate?: (update: GetStatusResponse) => void;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface SubmitQueueOptions {
|
|
72
|
+
input: Omit<GenerateImageRequest, 'model'>;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface GetResultOptions {
|
|
76
|
+
requestId: string;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface GetResultResponse {
|
|
80
|
+
data: GetResponseResponse;
|
|
81
|
+
requestId: string;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface StatusOptions {
|
|
85
|
+
requestId: string;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface StatusResponse {
|
|
89
|
+
data: GetStatusResponse;
|
|
90
|
+
requestId: string;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// --- Stream Types ---
|
|
94
|
+
export interface StreamOptions {
|
|
95
|
+
input: Omit<GenerateImageRequest, 'model'>;
|
|
96
|
+
// logs?: boolean; // Optional: if you want to request logs via stream if API supports it
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Represents an event received from the SSE stream.
|
|
101
|
+
* The `data` field is expected to be compatible with GetStatusResponse.
|
|
102
|
+
*/
|
|
103
|
+
export interface StreamEvent extends GetStatusResponse { // SSE events directly reflect status updates
|
|
104
|
+
// type: string; // Could be 'status', 'error', 'open', etc. if the stream sends typed messages.
|
|
105
|
+
// If the stream ONLY sends GetStatusResponse JSON, this might not be needed.
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface YetterStream extends AsyncIterable<StreamEvent> {
|
|
109
|
+
done(): Promise<GetResponseResponse>; // Resolves with the final image data
|
|
110
|
+
cancel(): Promise<void>; // Cancels the stream and attempts to cancel the underlying API request
|
|
111
|
+
getRequestId(): string; // Helper to get the request ID associated with the stream
|
|
112
|
+
}
|
package/tsconfig.json
ADDED