@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 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
+ }
@@ -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
+ };
@@ -0,0 +1,3 @@
1
+ export { YetterImageClient } from "./api.js";
2
+ export * from "./types.js";
3
+ export { yetter } from "./client.js";
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { YetterImageClient } from "./api.js";
2
+ export * from "./types.js";
3
+ export { yetter } from "./client.js";
@@ -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
@@ -0,0 +1,3 @@
1
+ export { YetterImageClient } from "./api.js";
2
+ export * from "./types.js";
3
+ export { yetter } from "./client.js";
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
@@ -0,0 +1,13 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2019",
4
+ "module": "ESNext",
5
+ "moduleResolution": "node",
6
+ "declaration": true,
7
+ "outDir": "dist",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true
11
+ },
12
+ "include": ["src/**/*"]
13
+ }