@yetter/client 0.0.12 → 0.0.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md 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).
@@ -48,6 +96,7 @@ This function submits an image generation request and long-polls for the result.
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
 
@@ -97,11 +146,18 @@ This function submits an image generation request and returns an async iterable
97
146
  **Features:**
98
147
 
99
148
  * Initiates a request and establishes an SSE connection.
149
+ * Automatically falls back to status polling if SSE transport repeatedly fails.
100
150
  * Provides an `AsyncIterator` (`Symbol.asyncIterator`) to loop through status events (`StreamEvent`).
101
151
  * 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.
102
152
  * A `cancel()` method: Requests cancellation on the backend; the stream naturally ends when the server emits `data` (with `status: "CANCELLED"`) followed by `event: done`.
103
153
  * A `getRequestId()` method: Returns the request ID for the stream.
104
154
 
155
+ **Transport options (`StreamOptions`):**
156
+
157
+ * `disableSse?: boolean` - skip SSE and use polling only.
158
+ * `pollIntervalMs?: number` - polling interval for fallback/forced polling (default: `2000`).
159
+ * `sseMaxConsecutiveErrors?: number` - number of transport errors tolerated before fallback (default: `3`).
160
+
105
161
  **Events (`StreamEvent`):**
106
162
  Each event pushed by the stream is an object typically including:
107
163
 
@@ -199,11 +255,11 @@ async function main() {
199
255
  console.log("Request ID:", request_id);
200
256
 
201
257
  if (request_id) {
202
- console.log(`\n--- Polling for status of Request ID: ${request_id} ---`);
258
+ console.log(`\n--- Polling for status of Request ID: ${request_id} (10-minute timeout) ---`);
203
259
  // Polling logic:
204
260
  let success = false;
205
261
  const startTime = Date.now();
206
- const timeoutMilliseconds = 3 * 60 * 1000; // 3 minutes
262
+ const timeoutMilliseconds = 10 * 60 * 1000; // 10 minutes
207
263
  const pollIntervalMilliseconds = 10 * 1000; // Poll every 10 seconds
208
264
 
209
265
  while (Date.now() - startTime < timeoutMilliseconds) {
@@ -259,7 +315,7 @@ const uploadResult = await yetter.uploadFile("./input-image.jpg", {
259
315
  const result = await yetter.subscribe("ytr-ai/model/i2i", {
260
316
  input: {
261
317
  prompt: "Transform to anime style",
262
- input_image_url: uploadResult.url,
318
+ image_url: [uploadResult.url],
263
319
  },
264
320
  });
265
321
  ```
package/dist/api.d.ts CHANGED
@@ -4,6 +4,9 @@ export declare class YetterImageClient {
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>;
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,67 +35,102 @@ 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,
68
133
  });
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
134
  }
75
135
  /**
76
136
  * Request presigned URL(s) for file upload
@@ -78,19 +138,14 @@ export class YetterImageClient {
78
138
  * @returns Presigned URL response with mode (single/multipart)
79
139
  */
80
140
  async getUploadUrl(body) {
81
- const res = await fetch(`${this.endpoint}/uploads`, {
141
+ return this.requestJson(`${this.endpoint}/uploads`, {
82
142
  method: "POST",
83
- headers: {
84
- "Content-Type": "application/json",
85
- Authorization: this.apiKey,
86
- },
143
+ headers: this.getAuthHeaders(),
87
144
  body: JSON.stringify(body),
145
+ }, {
146
+ maxRetries: 0,
147
+ errorPrefix: "Upload URL request failed",
88
148
  });
89
- if (!res.ok) {
90
- const errorText = await res.text();
91
- throw new Error(`Upload URL request failed (${res.status}): ${errorText}`);
92
- }
93
- return (await res.json());
94
149
  }
95
150
  /**
96
151
  * Notify server that upload is complete
@@ -98,18 +153,13 @@ export class YetterImageClient {
98
153
  * @returns Uploaded file metadata with public URL
99
154
  */
100
155
  async uploadComplete(body) {
101
- const res = await fetch(`${this.endpoint}/uploads/complete`, {
156
+ return this.requestJson(`${this.endpoint}/uploads/complete`, {
102
157
  method: "POST",
103
- headers: {
104
- "Content-Type": "application/json",
105
- Authorization: this.apiKey,
106
- },
158
+ headers: this.getAuthHeaders(),
107
159
  body: JSON.stringify(body),
160
+ }, {
161
+ maxRetries: 0,
162
+ errorPrefix: "Upload completion failed",
108
163
  });
109
- if (!res.ok) {
110
- const errorText = await res.text();
111
- throw new Error(`Upload completion failed (${res.status}): ${errorText}`);
112
- }
113
- return (await res.json());
114
164
  }
115
165
  }
package/dist/client.d.ts CHANGED
@@ -1,7 +1,32 @@
1
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,56 +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>;
13
- /**
14
- * Upload a file from the filesystem (Node.js) or File/Blob object (browser)
15
- *
16
- * @param fileOrPath File path (Node.js) or File/Blob object (browser)
17
- * @param options Upload configuration options
18
- * @returns Promise resolving to upload result with public URL
19
- *
20
- * @example
21
- * ```typescript
22
- * // Node.js
23
- * const result = await yetter.uploadFile("/path/to/image.jpg", {
24
- * onProgress: (pct) => console.log(`Upload: ${pct}%`)
25
- * });
26
- *
27
- * // Browser
28
- * const fileInput = document.querySelector('input[type="file"]');
29
- * const file = fileInput.files[0];
30
- * const result = await yetter.uploadFile(file, {
31
- * onProgress: (pct) => updateProgressBar(pct)
32
- * });
33
- * ```
34
- */
35
38
  static uploadFile(fileOrPath: string | File | Blob, options?: UploadOptions): Promise<UploadCompleteResponse>;
36
- /**
37
- * Upload a file from browser (File or Blob object)
38
- * This is an alias for uploadFile for better clarity in browser contexts
39
- */
40
39
  static uploadBlob(file: File | Blob, options?: UploadOptions): Promise<UploadCompleteResponse>;
41
- /**
42
- * Upload file from filesystem path (Node.js only)
43
- */
44
- private static _uploadFromPath;
45
- /**
46
- * Upload file from Blob/File object (browser)
47
- */
48
- private static _uploadFromBlob;
49
- /**
50
- * Upload file using single PUT request (Node.js, private helper)
51
- */
52
- private static _uploadFileSingle;
53
- /**
54
- * Upload file using multipart upload (Node.js, private helper)
55
- */
56
- private static _uploadFileMultipart;
57
- /**
58
- * Upload blob using single PUT request (browser, private helper)
59
- */
60
- private static _uploadBlobSingle;
61
- /**
62
- * Upload blob using multipart upload (browser, private helper)
63
- */
64
- private static _uploadBlobMultipart;
65
40
  }