@yetter/client 0.0.1 → 0.0.3
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 +220 -0
- package/dist/client.js +3 -3
- package/package.json +1 -1
- package/src/client.ts +3 -3
- package/stress.ts +135 -0
package/README.md
CHANGED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
# Yetter JS Client
|
|
2
|
+
|
|
3
|
+
The Yetter JS Client provides a convenient way to interact with the Yetter API for image generation. It supports different modes of operation: subscribing to real-time updates, submitting jobs to a queue, and streaming events.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @yetter/client
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Authentication
|
|
12
|
+
|
|
13
|
+
The client requires a Yetter API key for authentication. Ensure you have the `YTR_API_KEY` environment variable set:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
export YTR_API_KEY="your_api_key_here"
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Core Functionalities
|
|
20
|
+
|
|
21
|
+
The client is available via the `yetter` object imported from `yetter-js` (or the relevant path to `client.js`/`client.ts` if used directly).
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
import { yetter } from "@yetter/client";
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### 1. `yetter.subscribe()`
|
|
28
|
+
|
|
29
|
+
This function submits an image generation request and long-polls for the result. It provides updates on the queue status and logs if requested.
|
|
30
|
+
|
|
31
|
+
**Features:**
|
|
32
|
+
- Submits a generation request.
|
|
33
|
+
- Polls for status updates until the job is "COMPLETED" or "FAILED".
|
|
34
|
+
- Timeout after 30 minutes, with an attempt to cancel the job.
|
|
35
|
+
- Optional `onQueueUpdate` callback for real-time feedback on queue position and status.
|
|
36
|
+
- Optional `logs` flag to include logs in status updates.
|
|
37
|
+
|
|
38
|
+
**Example (from `examples/subscribe.ts`):**
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
import { yetter } from "@yetter/client";
|
|
42
|
+
|
|
43
|
+
async function main() {
|
|
44
|
+
const model = "ytr-ai/flux/dev"; // Replace with your desired model
|
|
45
|
+
try {
|
|
46
|
+
console.log("\n--- Starting Subscribe Test ---");
|
|
47
|
+
const result = await yetter.subscribe(model, {
|
|
48
|
+
input: {
|
|
49
|
+
prompt: "a vibrant coral reef, underwater photography",
|
|
50
|
+
},
|
|
51
|
+
logs: true,
|
|
52
|
+
onQueueUpdate: (update) => {
|
|
53
|
+
console.log(`[Queue Update] Status: ${update.status}, Position: ${update.queue_position}`);
|
|
54
|
+
if (update.status === "IN_PROGRESS" && update.logs) {
|
|
55
|
+
console.log("Logs:");
|
|
56
|
+
update.logs.map((log) => log.message).forEach(logMessage => console.log(` - ${logMessage}`));
|
|
57
|
+
} else if (update.status === "COMPLETED") {
|
|
58
|
+
console.log("Processing completed!");
|
|
59
|
+
} else if (update.status === "FAILED") {
|
|
60
|
+
console.error("Processing failed. Logs:", update.logs);
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
console.log("\n--- Subscribe Test Result ---");
|
|
66
|
+
console.log("Results:", result); // Contains image data, prompt, etc.
|
|
67
|
+
|
|
68
|
+
} catch (err: any) {
|
|
69
|
+
console.error("\n--- Subscribe Test Failed ---");
|
|
70
|
+
console.error("Error during subscribe:", err.message || err);
|
|
71
|
+
// ... error handling ...
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
main();
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### 2. `yetter.stream()`
|
|
79
|
+
|
|
80
|
+
This function submits an image generation request and returns an async iterable stream of events using Server-Sent Events (SSE). This allows for real-time updates on the job's progress, status, and logs.
|
|
81
|
+
|
|
82
|
+
**Features:**
|
|
83
|
+
- Initiates a request and establishes an SSE connection.
|
|
84
|
+
- Provides an `AsyncIterator` (`Symbol.asyncIterator`) to loop through status events (`StreamEvent`).
|
|
85
|
+
- A `done()` method: Returns a Promise that resolves with the final `GetResponseResponse` when the job is "COMPLETED", or rejects if it "FAILED" or the stream is prematurely closed.
|
|
86
|
+
- A `cancel()` method: Closes the stream and attempts to cancel the underlying image generation request.
|
|
87
|
+
- A `getRequestId()` method: Returns the request ID for the stream.
|
|
88
|
+
|
|
89
|
+
**Events (`StreamEvent`):**
|
|
90
|
+
Each event pushed by the stream is an object typically including:
|
|
91
|
+
- `status`: Current status (e.g., "IN_QUEUE", "IN_PROGRESS", "COMPLETED", "FAILED").
|
|
92
|
+
- `queue_position`: Current position in the queue.
|
|
93
|
+
- `logs`: Array of log messages if any.
|
|
94
|
+
- Other model-specific data.
|
|
95
|
+
|
|
96
|
+
**Example (from `examples/stream.ts`):**
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
import { yetter } from "@yetter/client"; // Adjust path as needed
|
|
100
|
+
|
|
101
|
+
async function main() {
|
|
102
|
+
const model = "ytr-ai/flux/dev";
|
|
103
|
+
let streamRequestId = "";
|
|
104
|
+
try {
|
|
105
|
+
console.log("\n--- Starting Stream Test ---");
|
|
106
|
+
const streamInstance = await yetter.stream(model, {
|
|
107
|
+
input: {
|
|
108
|
+
prompt: "a bioluminescent forest at night, fantasy art",
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
streamRequestId = streamInstance.getRequestId();
|
|
112
|
+
console.log(`Stream initiated for Request ID: ${streamRequestId}`);
|
|
113
|
+
|
|
114
|
+
// Iterate over stream events
|
|
115
|
+
for await (const event of streamInstance) {
|
|
116
|
+
console.log(`[STREAM EVENT - ${streamRequestId}] Status: ${event.status}, QPos: ${event.queue_position}`);
|
|
117
|
+
if (event.logs && event.logs.length > 0) {
|
|
118
|
+
console.log(` Logs for ${streamRequestId}:`);
|
|
119
|
+
event.logs.forEach(log => console.log(` - ${log.message}`));
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
console.log(`Stream for ${streamRequestId} finished iterating events.`);
|
|
123
|
+
|
|
124
|
+
// Wait for the final result from the done() method
|
|
125
|
+
const result = await streamInstance.done();
|
|
126
|
+
console.log("\n--- Stream Test Final Result ---");
|
|
127
|
+
console.log("Generated Images:", result.images);
|
|
128
|
+
|
|
129
|
+
} catch (err: any) {
|
|
130
|
+
console.error(`\n--- Stream Test Failed (Request ID: ${streamRequestId || 'UNKNOWN'}) ---`);
|
|
131
|
+
console.error("Error during stream test:", err.message || err);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
main();
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### 3. `yetter.queue`
|
|
139
|
+
|
|
140
|
+
This namespace provides methods for managing jobs in a queue-based workflow. This is useful if you want to submit a job and check its status or retrieve its result later, without maintaining a persistent connection.
|
|
141
|
+
|
|
142
|
+
#### `yetter.queue.submit(model, options)`
|
|
143
|
+
Submits a job to the queue.
|
|
144
|
+
|
|
145
|
+
- **`model`**: The model ID (e.g., `"ytr-ai/flux/dev"`).
|
|
146
|
+
- **`options.input`**: An object containing the input parameters for the model (e.g., `{ prompt: "your prompt" }`).
|
|
147
|
+
|
|
148
|
+
Returns a promise that resolves to an object containing `request_id`, `status`, and `queue_position`.
|
|
149
|
+
|
|
150
|
+
#### `yetter.queue.status(model, options)`
|
|
151
|
+
Checks the status of a previously submitted job.
|
|
152
|
+
|
|
153
|
+
- **`model`**: The model ID.
|
|
154
|
+
- **`options.requestId`**: The `request_id` obtained from `queue.submit()`.
|
|
155
|
+
|
|
156
|
+
Returns a promise that resolves to an object containing the status data and `requestId`.
|
|
157
|
+
|
|
158
|
+
#### `yetter.queue.result(model, options)`
|
|
159
|
+
Retrieves the result of a completed job.
|
|
160
|
+
|
|
161
|
+
- **`model`**: The model ID.
|
|
162
|
+
- **`options.requestId`**: The `request_id` of the completed job.
|
|
163
|
+
|
|
164
|
+
Returns a promise that resolves to an object containing the result data (e.g., images, prompt) and `requestId`.
|
|
165
|
+
|
|
166
|
+
**Example (from `examples/submit.ts`):**
|
|
167
|
+
|
|
168
|
+
```typescript
|
|
169
|
+
import { yetter } from "@yetter/client";
|
|
170
|
+
|
|
171
|
+
async function main() {
|
|
172
|
+
const model = "ytr-ai/flux/dev";
|
|
173
|
+
try {
|
|
174
|
+
console.log("\n--- Starting Queue Submit Test ---");
|
|
175
|
+
const { request_id } = await yetter.queue.submit(model, {
|
|
176
|
+
input: {
|
|
177
|
+
prompt: "a fluffy white kitten playing with a yarn ball",
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
console.log("Request ID:", request_id);
|
|
181
|
+
|
|
182
|
+
if (request_id) {
|
|
183
|
+
console.log(`\n--- Polling for status of Request ID: ${request_id} ---`);
|
|
184
|
+
// Polling logic:
|
|
185
|
+
let success = false;
|
|
186
|
+
const startTime = Date.now();
|
|
187
|
+
const timeoutMilliseconds = 3 * 60 * 1000; // 3 minutes
|
|
188
|
+
const pollIntervalMilliseconds = 10 * 1000; // Poll every 10 seconds
|
|
189
|
+
|
|
190
|
+
while (Date.now() - startTime < timeoutMilliseconds) {
|
|
191
|
+
const statusResult = await yetter.queue.status(model, { requestId: request_id });
|
|
192
|
+
const currentStatus = statusResult.data.status;
|
|
193
|
+
console.log(`[${new Date().toLocaleTimeString()}] Status: ${currentStatus}, QPos: ${statusResult.data.queue_position}`);
|
|
194
|
+
|
|
195
|
+
if (currentStatus === "COMPLETED") {
|
|
196
|
+
const finalResult = await yetter.queue.result(model, { requestId: request_id });
|
|
197
|
+
console.log("\n--- Get Result Test Succeeded ---");
|
|
198
|
+
console.log("Image Data:", finalResult.data.images);
|
|
199
|
+
success = true;
|
|
200
|
+
break;
|
|
201
|
+
} else if (currentStatus === "FAILED") {
|
|
202
|
+
console.error(`Request ${request_id} FAILED. Logs:`, statusResult.data.logs);
|
|
203
|
+
break;
|
|
204
|
+
}
|
|
205
|
+
await new Promise(resolve => setTimeout(resolve, pollIntervalMilliseconds));
|
|
206
|
+
}
|
|
207
|
+
if (!success) console.error(`Polling Timed Out or Failed for ${request_id}`);
|
|
208
|
+
}
|
|
209
|
+
} catch (err: any) {
|
|
210
|
+
console.error("\n--- Queue Submit Test Failed ---", err.message);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
main();
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
## Error Handling
|
|
218
|
+
|
|
219
|
+
The client functions generally throw errors for API issues, network problems, or failed generation requests. Ensure you wrap API calls in `try...catch` blocks to handle potential errors gracefully. Specific error messages or details (like logs for failed jobs) are often included in the thrown error object or the final status response.
|
|
220
|
+
The `stream()` function's `done()` promise will reject on failure or premature closure, and iterating the stream might also throw if connection errors occur.
|
package/dist/client.js
CHANGED
|
@@ -4,11 +4,11 @@ import { EventSourcePolyfill } from 'event-source-polyfill';
|
|
|
4
4
|
export class yetter {
|
|
5
5
|
static async subscribe(model, options) {
|
|
6
6
|
var _b;
|
|
7
|
-
if (!process.env.YTR_API_KEY) {
|
|
8
|
-
throw new Error("YTR_API_KEY
|
|
7
|
+
if (!process.env.YTR_API_KEY && !process.env.REACT_APP_YTR_API_KEY) {
|
|
8
|
+
throw new Error("YTR_API_KEY and REACT_APP_YTR_API_KEY are not set");
|
|
9
9
|
}
|
|
10
10
|
const client = new YetterImageClient({
|
|
11
|
-
apiKey: process.env.YTR_API_KEY,
|
|
11
|
+
apiKey: process.env.YTR_API_KEY || process.env.REACT_APP_YTR_API_KEY || "",
|
|
12
12
|
});
|
|
13
13
|
const generateResponse = await client.generateImage({
|
|
14
14
|
model: model,
|
package/package.json
CHANGED
package/src/client.ts
CHANGED
|
@@ -20,11 +20,11 @@ export class yetter {
|
|
|
20
20
|
model: string,
|
|
21
21
|
options: SubscribeOptions
|
|
22
22
|
): Promise<GetResponseResponse> {
|
|
23
|
-
if (!process.env.YTR_API_KEY) {
|
|
24
|
-
throw new Error("YTR_API_KEY
|
|
23
|
+
if (!process.env.YTR_API_KEY && !process.env.REACT_APP_YTR_API_KEY) {
|
|
24
|
+
throw new Error("YTR_API_KEY and REACT_APP_YTR_API_KEY are not set");
|
|
25
25
|
}
|
|
26
26
|
const client = new YetterImageClient({
|
|
27
|
-
apiKey: process.env.YTR_API_KEY,
|
|
27
|
+
apiKey: process.env.YTR_API_KEY || process.env.REACT_APP_YTR_API_KEY || "",
|
|
28
28
|
});
|
|
29
29
|
|
|
30
30
|
const generateResponse = await client.generateImage({
|
package/stress.ts
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { yetter } from "../src/client.js";
|
|
2
|
+
|
|
3
|
+
const CONCURRENCY_LEVEL = 5; // Number of streams to run in parallel
|
|
4
|
+
const MODEL_NAME = "ytr-ai/flux/dev"; // Model to use for streaming
|
|
5
|
+
|
|
6
|
+
let streamCounter = 0;
|
|
7
|
+
const activePromises: Promise<void>[] = [];
|
|
8
|
+
let shuttingDown = false;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Runs a single stream, measures latency, and logs results.
|
|
12
|
+
* @param id - A unique identifier for the stream.
|
|
13
|
+
*/
|
|
14
|
+
async function runStream(id: number): Promise<void> {
|
|
15
|
+
const startTime = Date.now();
|
|
16
|
+
let streamRequestId = "";
|
|
17
|
+
|
|
18
|
+
console.log(`[${new Date().toLocaleTimeString()}] [Stream ${id}] Starting...`);
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
const streamInstance = await yetter.stream(MODEL_NAME, {
|
|
22
|
+
input: {
|
|
23
|
+
prompt: `A beautiful landscape painting, style of Van Gogh, stream ${id}`,
|
|
24
|
+
// You can add other parameters like seed or num_inference_steps here
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
streamRequestId = streamInstance.getRequestId();
|
|
28
|
+
console.log(`[${new Date().toLocaleTimeString()}] [Stream ${id}] Initiated. Request ID: ${streamRequestId}`);
|
|
29
|
+
|
|
30
|
+
// We are primarily interested in the latency of done(), so event iteration can be minimal or skipped.
|
|
31
|
+
// for await (const event of streamInstance) {
|
|
32
|
+
// console.log(`[${new Date().toLocaleTimeString()}][STREAM EVENT - ${streamRequestId}] Status: ${event.status}, QPos: ${event.queue_position}`);
|
|
33
|
+
// }
|
|
34
|
+
|
|
35
|
+
// Wait for the final result from the done() method
|
|
36
|
+
await streamInstance.done();
|
|
37
|
+
const endTime = Date.now();
|
|
38
|
+
const latency = endTime - startTime;
|
|
39
|
+
console.log(`[${new Date().toLocaleTimeString()}] [Stream ${id}] Finished. Request ID: ${streamRequestId}. Latency: ${latency}ms`);
|
|
40
|
+
|
|
41
|
+
} catch (err: any) {
|
|
42
|
+
const endTime = Date.now();
|
|
43
|
+
const latency = endTime - startTime; // Still record latency up to the point of failure
|
|
44
|
+
console.error(`[${new Date().toLocaleTimeString()}] [Stream ${id}] Failed. Request ID: ${streamRequestId || 'UNKNOWN'}. Latency: ${latency}ms. Error: ${err.message || err}`);
|
|
45
|
+
// Optionally, rethrow or handle more specifically if needed
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Launches a new stream if concurrency limit is not reached and not shutting down.
|
|
51
|
+
* Manages the activePromises array.
|
|
52
|
+
*/
|
|
53
|
+
async function launchStreamIfNotBusy(): Promise<void> {
|
|
54
|
+
if (shuttingDown || activePromises.length >= CONCURRENCY_LEVEL) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
streamCounter++;
|
|
59
|
+
const currentStreamId = streamCounter;
|
|
60
|
+
|
|
61
|
+
console.log(`[${new Date().toLocaleTimeString()}] Launching stream ${currentStreamId}. Active: ${activePromises.length}/${CONCURRENCY_LEVEL}`);
|
|
62
|
+
|
|
63
|
+
const streamTask = runStream(currentStreamId);
|
|
64
|
+
activePromises.push(streamTask);
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
await streamTask;
|
|
68
|
+
} catch (e) {
|
|
69
|
+
// Errors are logged within runStream
|
|
70
|
+
} finally {
|
|
71
|
+
const index = activePromises.indexOf(streamTask);
|
|
72
|
+
if (index > -1) {
|
|
73
|
+
activePromises.splice(index, 1);
|
|
74
|
+
}
|
|
75
|
+
console.log(`[${new Date().toLocaleTimeString()}] Stream ${currentStreamId} completed. Active: ${activePromises.length}/${CONCURRENCY_LEVEL}`);
|
|
76
|
+
// After a stream finishes, try to fill its slot immediately
|
|
77
|
+
if (!shuttingDown) {
|
|
78
|
+
launchStreamIfNotBusy(); // Non-blocking call to fill the slot
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Fills available slots up to the CONCURRENCY_LEVEL.
|
|
85
|
+
*/
|
|
86
|
+
function fillSlots() {
|
|
87
|
+
while (!shuttingDown && activePromises.length < CONCURRENCY_LEVEL) {
|
|
88
|
+
// launchStreamIfNotBusy is async but we don't await it here
|
|
89
|
+
// as we want to launch multiple streams in parallel.
|
|
90
|
+
// It handles its own addition to activePromises.
|
|
91
|
+
launchStreamIfNotBusy();
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Main function to run the stress test.
|
|
97
|
+
*/
|
|
98
|
+
async function mainStressTest() {
|
|
99
|
+
console.log("--- Starting Yetter Stream Stress Test ---");
|
|
100
|
+
console.log(`Concurrency Level (N): ${CONCURRENCY_LEVEL}`);
|
|
101
|
+
console.log(`Target Model: ${MODEL_NAME}`);
|
|
102
|
+
console.log("Press Ctrl+C to stop gracefully (will finish active streams).");
|
|
103
|
+
|
|
104
|
+
process.on('SIGINT', async () => {
|
|
105
|
+
console.log('\nSIGINT received. Gracefully shutting down...');
|
|
106
|
+
shuttingDown = true;
|
|
107
|
+
console.log(`No new streams will be launched. Waiting for ${activePromises.length} active stream(s) to complete...`);
|
|
108
|
+
// The main loop will exit once activePromises is empty after shuttingDown is true.
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// Initial fill
|
|
112
|
+
fillSlots();
|
|
113
|
+
|
|
114
|
+
// Keep the script running and responsive to shutdown, also periodically check to fill slots
|
|
115
|
+
// in case some mechanism external to stream completion is needed (though launchStreamIfNotBusy's finally block should cover it)
|
|
116
|
+
while (true) {
|
|
117
|
+
if (shuttingDown && activePromises.length === 0) {
|
|
118
|
+
break; // Exit condition: shutting down and all streams are done.
|
|
119
|
+
}
|
|
120
|
+
// Periodically try to fill slots, mainly as a fallback or if initial fills didn't max out.
|
|
121
|
+
// The primary mechanism for refilling is within launchStreamIfNotBusy's finally block.
|
|
122
|
+
if (!shuttingDown) {
|
|
123
|
+
fillSlots();
|
|
124
|
+
}
|
|
125
|
+
await new Promise(resolve => setTimeout(resolve, 200)); // Interval for the main loop check
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
console.log('--- Stress Test Finished ---');
|
|
129
|
+
console.log(`Total streams launched during the session: ${streamCounter}`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
mainStressTest().catch(err => {
|
|
133
|
+
console.error("Unhandled error in mainStressTest:", err);
|
|
134
|
+
process.exit(1);
|
|
135
|
+
});
|