@yetter/client 0.0.11 → 0.0.13

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 CHANGED
@@ -8,6 +8,16 @@ The Yetter JS Client provides a convenient way to interact with the Yetter API f
8
8
  npm install @yetter/client
9
9
  ```
10
10
 
11
+ ## Testing
12
+
13
+ Run the local test suite:
14
+
15
+ ```bash
16
+ npm test
17
+ ```
18
+
19
+ Tests are automatically omitted from npm releases because `package.json` uses a `files` whitelist (`dist`, `README.md`).
20
+
11
21
  ## Authentication
12
22
 
13
23
  The client requires a Yetter API key for authentication. Ensure you have the `YTR_API_KEY` or `REACT_APP_YTR_API_KEY` environment variable set:
@@ -30,6 +40,44 @@ yetter.configure({
30
40
 
31
41
  If `yetter.configure()` is used, it will override any API key found in environment variables for subsequent API calls. If neither environment variables are set nor `yetter.configure()` is called with an API key, an error will be thrown when attempting to make an API call.
32
42
 
43
+ ## Instance Client (Recommended for Concurrent Apps)
44
+
45
+ For server or multi-tenant workloads, prefer creating isolated client instances instead of sharing global static state.
46
+
47
+ ```typescript
48
+ import { YetterClient } from "@yetter/client";
49
+
50
+ const teamAClient = new YetterClient({ apiKey: process.env.TEAM_A_YTR_API_KEY! });
51
+ const teamBClient = new YetterClient({ apiKey: process.env.TEAM_B_YTR_API_KEY! });
52
+
53
+ const result = await teamAClient.subscribe("ytr-ai/qwen/image-edit/i2i", {
54
+ input: {
55
+ prompt: "Transform this image to watercolor painting style",
56
+ image_url: ["https://example.com/input.jpg"],
57
+ num_inference_steps: 28,
58
+ },
59
+ });
60
+ ```
61
+
62
+ ## Running i2i Example Scripts
63
+
64
+ For image-to-image models (such as `ytr-ai/qwen/image-edit/i2i`), provide both a prompt and an input image.
65
+
66
+ ```bash
67
+ export YTR_API_KEY="your_api_key_here"
68
+ export YTR_MODEL="ytr-ai/qwen/image-edit/i2i"
69
+ export YTR_IMAGE_PATH="./bowow2.jpeg"
70
+ export YTR_PROMPT="Transform this image to watercolor painting style"
71
+ ```
72
+
73
+ Then run one of:
74
+
75
+ ```bash
76
+ npm run test:submit
77
+ npm run test:subscribe
78
+ npm run test:stream
79
+ ```
80
+
33
81
  ## Core Functionalities
34
82
 
35
83
  The client is available via the `yetter` object imported from `yetter-js` (or the relevant path to `client.js`/`client.ts` if used directly).
@@ -44,10 +92,11 @@ This function submits an image generation request and long-polls for the result.
44
92
 
45
93
  **Features:**
46
94
  - Submits a generation request.
47
- - Polls for status updates until the job is "COMPLETED" or "FAILED".
95
+ - Polls for status updates until the job is "COMPLETED" or "ERROR".
48
96
  - Timeout after 30 minutes, with an attempt to cancel the job.
49
97
  - Optional `onQueueUpdate` callback for real-time feedback on queue position and status.
50
98
  - Optional `logs` flag to include logs in status updates.
99
+ - Optional `pollIntervalMs` to control status polling interval (default: 2000ms).
51
100
 
52
101
  **Example (from `examples/subscribe.ts`):**
53
102
 
@@ -71,7 +120,7 @@ async function main() {
71
120
  update.logs.map((log) => log.message).forEach(logMessage => console.log(` - ${logMessage}`));
72
121
  } else if (update.status === "COMPLETED") {
73
122
  console.log("Processing completed!");
74
- } else if (update.status === "FAILED") {
123
+ } else if (update.status === "ERROR") {
75
124
  console.error("Processing failed. Logs:", update.logs);
76
125
  }
77
126
  },
@@ -95,18 +144,18 @@ main();
95
144
  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.
96
145
 
97
146
  **Features:**
98
- - Initiates a request and establishes an SSE connection.
99
- - Provides an `AsyncIterator` (`Symbol.asyncIterator`) to loop through status events (`StreamEvent`).
100
- - 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.
101
- - A `cancel()` method: Closes the stream and attempts to cancel the underlying image generation request.
102
- - A `getRequestId()` method: Returns the request ID for the stream.
147
+
148
+ * Initiates a request and establishes an SSE connection.
149
+ * Provides an `AsyncIterator` (`Symbol.asyncIterator`) to loop through status events (`StreamEvent`).
150
+ * A `done()` method: Returns a Promise that resolves with the final `GetResponseResponse` **after the server emits `event: done`** (successful completion), or rejects if the server emits `event: error` or the final response cannot be fetched.
151
+ * A `cancel()` method: Requests cancellation on the backend; the stream naturally ends when the server emits `data` (with `status: "CANCELLED"`) followed by `event: done`.
152
+ * A `getRequestId()` method: Returns the request ID for the stream.
103
153
 
104
154
  **Events (`StreamEvent`):**
105
155
  Each event pushed by the stream is an object typically including:
106
- - `status`: Current status (e.g., "IN_QUEUE", "IN_PROGRESS", "COMPLETED", "FAILED").
107
- - `queue_position`: Current position in the queue.
108
- - `logs`: Array of log messages if any.
109
- - Other model-specific data.
156
+
157
+ * `status`: Current status (e.g., `"IN_QUEUE"`, `"IN_PROGRESS"`, `"COMPLETED"`, `"CANCELLED"`, `"ERROR"`).
158
+ * Other model-specific data.
110
159
 
111
160
  **Example (from `examples/stream.ts`):**
112
161
 
@@ -127,9 +176,11 @@ async function main() {
127
176
  streamRequestId = streamInstance.getRequestId();
128
177
  console.log(`Stream initiated for Request ID: ${streamRequestId}`);
129
178
 
130
- // Iterate over stream events
179
+ // Iterate over stream events (informational; termination is driven by server 'done'/'error' events)
131
180
  for await (const event of streamInstance) {
132
- console.log(`[STREAM EVENT - ${streamRequestId}] Status: ${event.status}, QPos: ${event.queue_position}`);
181
+ console.log(
182
+ `[STREAM EVENT - ${streamRequestId}] Status: ${event.status}, QPos: ${event.queue_position}`
183
+ );
133
184
  if (event.logs && event.logs.length > 0) {
134
185
  console.log(` Logs for ${streamRequestId}:`);
135
186
  event.logs.forEach(log => console.log(` - ${log.message}`));
@@ -137,7 +188,7 @@ async function main() {
137
188
  }
138
189
  console.log(`Stream for ${streamRequestId} finished iterating events.`);
139
190
 
140
- // Wait for the final result from the done() method
191
+ // Final result becomes available after server emits `event: done`
141
192
  const result = await streamInstance.done();
142
193
  console.log("\n--- Stream Test Final Result ---");
143
194
  console.log("Generated Images:", result.images);
@@ -197,11 +248,11 @@ async function main() {
197
248
  console.log("Request ID:", request_id);
198
249
 
199
250
  if (request_id) {
200
- console.log(`\n--- Polling for status of Request ID: ${request_id} ---`);
251
+ console.log(`\n--- Polling for status of Request ID: ${request_id} (10-minute timeout) ---`);
201
252
  // Polling logic:
202
253
  let success = false;
203
254
  const startTime = Date.now();
204
- const timeoutMilliseconds = 3 * 60 * 1000; // 3 minutes
255
+ const timeoutMilliseconds = 10 * 60 * 1000; // 10 minutes
205
256
  const pollIntervalMilliseconds = 10 * 1000; // Poll every 10 seconds
206
257
 
207
258
  while (Date.now() - startTime < timeoutMilliseconds) {
@@ -215,7 +266,7 @@ async function main() {
215
266
  console.log("Image Data:", finalResult.data.images);
216
267
  success = true;
217
268
  break;
218
- } else if (currentStatus === "FAILED") {
269
+ } else if (currentStatus === "ERROR") {
219
270
  console.error(`Request ${request_id} FAILED. Logs:`, statusResult.data.logs);
220
271
  break;
221
272
  }
@@ -231,6 +282,59 @@ async function main() {
231
282
  main();
232
283
  ```
233
284
 
285
+ ### 4. Uploading Input Images
286
+
287
+ For models that require an input image (e.g., image-to-image, style transfer), you can upload your image using `yetter.uploadFile()` (Node.js) or `yetter.uploadBlob()` (browser).
288
+
289
+ **Features:**
290
+ - Support for both Node.js filesystem paths and browser File/Blob objects
291
+ - Automatic single-part or multipart upload based on file size
292
+ - Progress tracking with optional callback
293
+ - Returns a public URL to use in generation requests
294
+
295
+ #### Node.js Example
296
+
297
+ ```typescript
298
+ import { yetter } from "@yetter/client";
299
+
300
+ yetter.configure({ apiKey: process.env.YTR_API_KEY });
301
+
302
+ // Upload image
303
+ const uploadResult = await yetter.uploadFile("./input-image.jpg", {
304
+ onProgress: (percent) => console.log(`Upload: ${percent}%`),
305
+ });
306
+
307
+ // Use in generation
308
+ const result = await yetter.subscribe("ytr-ai/model/i2i", {
309
+ input: {
310
+ prompt: "Transform to anime style",
311
+ image_url: [uploadResult.url],
312
+ },
313
+ });
314
+ ```
315
+
316
+ #### Browser Example
317
+
318
+ ```typescript
319
+ // HTML: <input type="file" id="imageInput" accept="image/*">
320
+
321
+ const fileInput = document.getElementById('imageInput') as HTMLInputElement;
322
+ fileInput.addEventListener('change', async () => {
323
+ const file = fileInput.files![0];
324
+
325
+ const uploadResult = await yetter.uploadBlob(file, {
326
+ onProgress: (pct) => updateProgressBar(pct),
327
+ });
328
+
329
+ console.log("Uploaded:", uploadResult.url);
330
+ });
331
+ ```
332
+
333
+ **Upload Options:**
334
+ - `onProgress?: (progress: number) => void` - Progress callback (0-100)
335
+ - `timeout?: number` - Timeout in milliseconds (default: 5 minutes)
336
+ - `filename?: string` - Custom filename (browser uploads)
337
+
234
338
  ## Error Handling
235
339
 
236
340
  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.
package/dist/api.d.ts CHANGED
@@ -1,11 +1,26 @@
1
- import { ClientOptions, GenerateRequest, GenerateResponse, GetStatusRequest, GetStatusResponse, CancelRequest, CancelResponse, GetResponseRequest, GetResponseResponse } from "./types";
1
+ import { ClientOptions, GenerateRequest, GenerateResponse, GetStatusRequest, GetStatusResponse, CancelRequest, CancelResponse, GetResponseRequest, GetResponseResponse, GetUploadUrlRequest, GetUploadUrlResponse, UploadCompleteRequest, UploadCompleteResponse } from "./types";
2
2
  export declare class YetterImageClient {
3
3
  private apiKey;
4
4
  private endpoint;
5
5
  constructor(options: ClientOptions);
6
6
  getApiEndpoint(): string;
7
+ private getAuthHeaders;
8
+ private readErrorBody;
9
+ private requestJson;
7
10
  generate(body: GenerateRequest): Promise<GenerateResponse>;
8
11
  getStatus(body: GetStatusRequest): Promise<GetStatusResponse>;
9
12
  cancel(body: CancelRequest): Promise<CancelResponse>;
10
13
  getResponse(body: GetResponseRequest): Promise<GetResponseResponse>;
14
+ /**
15
+ * Request presigned URL(s) for file upload
16
+ * @param body Upload request parameters
17
+ * @returns Presigned URL response with mode (single/multipart)
18
+ */
19
+ getUploadUrl(body: GetUploadUrlRequest): Promise<GetUploadUrlResponse>;
20
+ /**
21
+ * Notify server that upload is complete
22
+ * @param body Completion request with S3 key
23
+ * @returns Uploaded file metadata with public URL
24
+ */
25
+ uploadComplete(body: UploadCompleteRequest): Promise<UploadCompleteResponse>;
11
26
  }
package/dist/api.js CHANGED
@@ -1,4 +1,29 @@
1
1
  import fetch from "cross-fetch";
2
+ const DEFAULT_TIMEOUT_MS = 30000;
3
+ const MAX_RETRY_DELAY_MS = 5000;
4
+ const BASE_RETRY_DELAY_MS = 300;
5
+ const RETRYABLE_STATUS_CODES = new Set([408, 425, 429, 500, 502, 503, 504]);
6
+ function sleep(ms) {
7
+ return new Promise((resolve) => setTimeout(resolve, ms));
8
+ }
9
+ function isAbortError(error) {
10
+ return (error === null || error === void 0 ? void 0 : error.name) === "AbortError";
11
+ }
12
+ function isLikelyNetworkError(error) {
13
+ if (!error) {
14
+ return false;
15
+ }
16
+ if (error instanceof TypeError) {
17
+ return true;
18
+ }
19
+ const message = String(error.message || "");
20
+ return /(ECONNRESET|ECONNREFUSED|ETIMEDOUT|ENOTFOUND|EAI_AGAIN|NetworkError|network request failed)/i.test(message);
21
+ }
22
+ function getRetryDelayMs(attempt) {
23
+ const backoff = Math.min(MAX_RETRY_DELAY_MS, BASE_RETRY_DELAY_MS * (2 ** attempt));
24
+ const jitter = Math.floor(Math.random() * 150);
25
+ return backoff + jitter;
26
+ }
2
27
  export class YetterImageClient {
3
28
  constructor(options) {
4
29
  if (!options.apiKey) {
@@ -10,66 +35,131 @@ export class YetterImageClient {
10
35
  getApiEndpoint() {
11
36
  return this.endpoint;
12
37
  }
38
+ getAuthHeaders() {
39
+ return {
40
+ "Content-Type": "application/json",
41
+ Authorization: this.apiKey,
42
+ };
43
+ }
44
+ async readErrorBody(res) {
45
+ try {
46
+ const text = await res.text();
47
+ return text || res.statusText || "Unknown API error";
48
+ }
49
+ catch {
50
+ return res.statusText || "Unknown API error";
51
+ }
52
+ }
53
+ async requestJson(url, init, options = {}) {
54
+ var _a, _b, _c;
55
+ const maxRetries = (_a = options.maxRetries) !== null && _a !== void 0 ? _a : 0;
56
+ const timeoutMs = (_b = options.timeoutMs) !== null && _b !== void 0 ? _b : DEFAULT_TIMEOUT_MS;
57
+ const errorPrefix = (_c = options.errorPrefix) !== null && _c !== void 0 ? _c : "API error";
58
+ for (let attempt = 0;; attempt += 1) {
59
+ const controller = new AbortController();
60
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
61
+ try {
62
+ const res = await fetch(url, {
63
+ ...init,
64
+ signal: controller.signal,
65
+ });
66
+ if (!res.ok) {
67
+ const errorText = await this.readErrorBody(res);
68
+ const shouldRetry = attempt < maxRetries && RETRYABLE_STATUS_CODES.has(res.status);
69
+ if (shouldRetry) {
70
+ await sleep(getRetryDelayMs(attempt));
71
+ continue;
72
+ }
73
+ throw new Error(`${errorPrefix} (${res.status}): ${errorText}`);
74
+ }
75
+ return (await res.json());
76
+ }
77
+ catch (error) {
78
+ const retryableNetworkError = isAbortError(error) || isLikelyNetworkError(error);
79
+ const shouldRetry = attempt < maxRetries && retryableNetworkError;
80
+ if (shouldRetry) {
81
+ await sleep(getRetryDelayMs(attempt));
82
+ continue;
83
+ }
84
+ if (isAbortError(error)) {
85
+ throw new Error(`${errorPrefix}: request timed out after ${timeoutMs}ms`);
86
+ }
87
+ throw error;
88
+ }
89
+ finally {
90
+ clearTimeout(timeout);
91
+ }
92
+ }
93
+ }
13
94
  async generate(body) {
95
+ if (!body.model) {
96
+ throw new Error("`model` is required in generate request body");
97
+ }
14
98
  const url = `${this.endpoint}/${body.model}`;
15
- const res = await fetch(url, {
99
+ return this.requestJson(url, {
16
100
  method: "POST",
17
- headers: {
18
- "Content-Type": "application/json",
19
- Authorization: this.apiKey,
20
- },
101
+ headers: this.getAuthHeaders(),
21
102
  body: JSON.stringify(body),
103
+ }, {
104
+ maxRetries: 0,
22
105
  });
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
106
  }
29
107
  async getStatus(body) {
30
108
  const url = new URL(body.url);
31
109
  if (body.logs) {
32
110
  url.searchParams.append('logs', '1');
33
111
  }
34
- const res = await fetch(url.toString(), {
112
+ return this.requestJson(url.toString(), {
35
113
  method: "GET",
36
- headers: {
37
- "Content-Type": "application/json",
38
- Authorization: this.apiKey,
39
- },
114
+ headers: this.getAuthHeaders(),
115
+ }, {
116
+ maxRetries: 4,
40
117
  });
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
118
  }
47
119
  async cancel(body) {
48
- const res = await fetch(body.url, {
120
+ return this.requestJson(body.url, {
49
121
  method: "PUT",
50
- headers: {
51
- "Content-Type": "application/json",
52
- Authorization: this.apiKey,
53
- },
122
+ headers: this.getAuthHeaders(),
123
+ }, {
124
+ maxRetries: 3,
54
125
  });
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
126
  }
61
127
  async getResponse(body) {
62
- const res = await fetch(body.url, {
128
+ return this.requestJson(body.url, {
63
129
  method: "GET",
64
- headers: {
65
- "Content-Type": "application/json",
66
- Authorization: this.apiKey,
67
- },
130
+ headers: this.getAuthHeaders(),
131
+ }, {
132
+ maxRetries: 4,
133
+ });
134
+ }
135
+ /**
136
+ * Request presigned URL(s) for file upload
137
+ * @param body Upload request parameters
138
+ * @returns Presigned URL response with mode (single/multipart)
139
+ */
140
+ async getUploadUrl(body) {
141
+ return this.requestJson(`${this.endpoint}/uploads`, {
142
+ method: "POST",
143
+ headers: this.getAuthHeaders(),
144
+ body: JSON.stringify(body),
145
+ }, {
146
+ maxRetries: 0,
147
+ errorPrefix: "Upload URL request failed",
148
+ });
149
+ }
150
+ /**
151
+ * Notify server that upload is complete
152
+ * @param body Completion request with S3 key
153
+ * @returns Uploaded file metadata with public URL
154
+ */
155
+ async uploadComplete(body) {
156
+ return this.requestJson(`${this.endpoint}/uploads/complete`, {
157
+ method: "POST",
158
+ headers: this.getAuthHeaders(),
159
+ body: JSON.stringify(body),
160
+ }, {
161
+ maxRetries: 0,
162
+ errorPrefix: "Upload completion failed",
68
163
  });
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
164
  }
75
165
  }
package/dist/client.d.ts CHANGED
@@ -1,7 +1,32 @@
1
- import { ClientOptions, GetResponseResponse, SubscribeOptions, GenerateResponse, SubmitQueueOptions, GetResultOptions, GetResultResponse, StatusOptions, StatusResponse, StreamOptions, YetterStream } from "./types.js";
1
+ import { ClientOptions, GetResponseResponse, SubscribeOptions, GenerateResponse, SubmitQueueOptions, GetResultOptions, GetResultResponse, StatusOptions, StatusResponse, StreamOptions, YetterStream, UploadOptions, UploadCompleteResponse } from "./types.js";
2
+ export declare class YetterClient {
3
+ private apiKey;
4
+ private endpoint;
5
+ constructor(options?: Partial<ClientOptions>);
6
+ private setApiKey;
7
+ private assertApiKeyConfigured;
8
+ private createApiClient;
9
+ private getUploadTimeoutMs;
10
+ private putWithTimeout;
11
+ configure(options: ClientOptions): void;
12
+ subscribe(model: string, options: SubscribeOptions): Promise<GetResponseResponse>;
13
+ readonly queue: {
14
+ submit: (model: string, options: SubmitQueueOptions) => Promise<GenerateResponse>;
15
+ status: (model: string, options: StatusOptions) => Promise<StatusResponse>;
16
+ result: (model: string, options: GetResultOptions) => Promise<GetResultResponse>;
17
+ };
18
+ stream(model: string, options: StreamOptions): Promise<YetterStream>;
19
+ uploadFile(fileOrPath: string | File | Blob, options?: UploadOptions): Promise<UploadCompleteResponse>;
20
+ uploadBlob(file: File | Blob, options?: UploadOptions): Promise<UploadCompleteResponse>;
21
+ private _uploadFromPath;
22
+ private _uploadFromBlob;
23
+ private _uploadFileSingle;
24
+ private _uploadFileMultipart;
25
+ private _uploadBlobSingle;
26
+ private _uploadBlobMultipart;
27
+ }
2
28
  export declare class yetter {
3
- private static apiKey;
4
- private static endpoint;
29
+ private static client;
5
30
  static configure(options: ClientOptions): void;
6
31
  static subscribe(model: string, options: SubscribeOptions): Promise<GetResponseResponse>;
7
32
  static queue: {
@@ -10,4 +35,6 @@ export declare class yetter {
10
35
  result: (model: string, options: GetResultOptions) => Promise<GetResultResponse>;
11
36
  };
12
37
  static stream(model: string, options: StreamOptions): Promise<YetterStream>;
38
+ static uploadFile(fileOrPath: string | File | Blob, options?: UploadOptions): Promise<UploadCompleteResponse>;
39
+ static uploadBlob(file: File | Blob, options?: UploadOptions): Promise<UploadCompleteResponse>;
13
40
  }