waterlight 0.1.0 → 0.2.0

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
@@ -145,6 +145,8 @@ try {
145
145
  |-----------|---------|---------|
146
146
  | `apiKey` | `WATERLIGHT_API_KEY` | — (required) |
147
147
  | `baseUrl` | `WATERLIGHT_BASE_URL` | `https://api.waterlight.ai` |
148
+ | `timeout` | — | `120000` (ms) |
149
+ | `maxRetries` | — | `2` |
148
150
 
149
151
  ```typescript
150
152
  // Using env var
@@ -155,6 +157,8 @@ const client = new Waterlight(); // picks up from env
155
157
  const client = new Waterlight({
156
158
  apiKey: 'wl-...',
157
159
  baseUrl: 'https://custom.endpoint.com',
160
+ timeout: 60_000, // 60s timeout
161
+ maxRetries: 3, // retry 429/5xx up to 3 times
158
162
  });
159
163
  ```
160
164
 
package/dist/client.d.ts CHANGED
@@ -89,6 +89,8 @@ declare class Billing {
89
89
  export declare class Waterlight {
90
90
  readonly apiKey: string;
91
91
  readonly baseUrl: string;
92
+ readonly timeout: number;
93
+ readonly maxRetries: number;
92
94
  readonly chat: Chat;
93
95
  readonly embeddings: Embeddings;
94
96
  readonly models: Models;
@@ -96,8 +98,11 @@ export declare class Waterlight {
96
98
  constructor(opts?: {
97
99
  apiKey?: string;
98
100
  baseUrl?: string;
101
+ timeout?: number;
102
+ maxRetries?: number;
99
103
  });
100
104
  private _post;
101
105
  private _get;
106
+ private _request;
102
107
  }
103
108
  export {};
package/dist/client.js CHANGED
@@ -4,16 +4,26 @@ exports.Waterlight = void 0;
4
4
  const errors_1 = require("./errors");
5
5
  const streaming_1 = require("./streaming");
6
6
  const DEFAULT_BASE_URL = 'https://api.waterlight.ai';
7
- function handleError(status, body) {
7
+ const DEFAULT_TIMEOUT = 120000;
8
+ const DEFAULT_MAX_RETRIES = 2;
9
+ const RETRYABLE_STATUS = new Set([429, 500, 502, 503, 504]);
10
+ function sleep(ms) {
11
+ return new Promise(resolve => setTimeout(resolve, ms));
12
+ }
13
+ function handleError(status, body, headers) {
8
14
  const errObj = body?.error;
9
15
  const msg = (typeof errObj === 'object' ? errObj?.message : errObj) ?? 'Request failed';
16
+ const requestId = headers?.get('x-request-id') ?? undefined;
10
17
  if (status === 401)
11
- throw new errors_1.AuthenticationError(msg);
12
- if (status === 429)
13
- throw new errors_1.RateLimitError(msg);
18
+ throw new errors_1.AuthenticationError(msg, requestId);
19
+ if (status === 429) {
20
+ const ra = headers?.get('retry-after');
21
+ const retryAfter = ra ? parseFloat(ra) : undefined;
22
+ throw new errors_1.RateLimitError(msg, retryAfter, requestId);
23
+ }
14
24
  if (status === 402)
15
- throw new errors_1.InsufficientCreditsError(msg);
16
- throw new errors_1.APIError(msg, status);
25
+ throw new errors_1.InsufficientCreditsError(msg, requestId);
26
+ throw new errors_1.APIError(msg, status, requestId);
17
27
  }
18
28
  /** Chat completions namespace. */
19
29
  class Completions {
@@ -22,7 +32,7 @@ class Completions {
22
32
  }
23
33
  create(params) {
24
34
  if (params.stream) {
25
- return new streaming_1.Stream(`${this.client.baseUrl}/v1/chat/completions`, this.client.apiKey, params);
35
+ return new streaming_1.Stream(`${this.client.baseUrl}/v1/chat/completions`, this.client.apiKey, params, this.client.timeout);
26
36
  }
27
37
  return this.client['_post']('/v1/chat/completions', { ...params, stream: false });
28
38
  }
@@ -96,37 +106,59 @@ class Waterlight {
96
106
  }
97
107
  this.apiKey = key;
98
108
  this.baseUrl = (opts.baseUrl ?? process.env.WATERLIGHT_BASE_URL ?? DEFAULT_BASE_URL).replace(/\/+$/, '');
109
+ this.timeout = opts.timeout ?? DEFAULT_TIMEOUT;
110
+ this.maxRetries = opts.maxRetries ?? DEFAULT_MAX_RETRIES;
99
111
  this.chat = new Chat(this);
100
112
  this.embeddings = new Embeddings(this);
101
113
  this.models = new Models(this);
102
114
  this.billing = new Billing(this);
103
115
  }
104
116
  async _post(path, body) {
105
- const res = await fetch(`${this.baseUrl}${path}`, {
106
- method: 'POST',
107
- headers: {
108
- 'Authorization': `Bearer ${this.apiKey}`,
109
- 'Content-Type': 'application/json',
110
- 'User-Agent': 'waterlight-node/0.1.0',
111
- },
112
- body: JSON.stringify(body),
113
- });
114
- const data = await res.json();
115
- if (!res.ok)
116
- handleError(res.status, data);
117
- return data;
117
+ return this._request('POST', path, body);
118
118
  }
119
119
  async _get(path) {
120
- const res = await fetch(`${this.baseUrl}${path}`, {
121
- headers: {
122
- 'Authorization': `Bearer ${this.apiKey}`,
123
- 'User-Agent': 'waterlight-node/0.1.0',
124
- },
125
- });
126
- const data = await res.json();
127
- if (!res.ok)
128
- handleError(res.status, data);
129
- return data;
120
+ return this._request('GET', path);
121
+ }
122
+ async _request(method, path, body) {
123
+ const url = `${this.baseUrl}${path}`;
124
+ let attempt = 0;
125
+ while (true) {
126
+ const controller = new AbortController();
127
+ const timer = setTimeout(() => controller.abort(), this.timeout);
128
+ try {
129
+ const res = await fetch(url, {
130
+ method,
131
+ headers: {
132
+ 'Authorization': `Bearer ${this.apiKey}`,
133
+ ...(body ? { 'Content-Type': 'application/json' } : {}),
134
+ 'User-Agent': 'waterlight-node/0.2.0',
135
+ },
136
+ ...(body ? { body: JSON.stringify(body) } : {}),
137
+ signal: controller.signal,
138
+ });
139
+ clearTimeout(timer);
140
+ if (!res.ok) {
141
+ if (RETRYABLE_STATUS.has(res.status) && attempt < this.maxRetries) {
142
+ const retryAfter = res.headers.get('retry-after');
143
+ const delay = retryAfter ? parseFloat(retryAfter) * 1000 : 500 * 2 ** attempt;
144
+ await sleep(delay);
145
+ attempt++;
146
+ continue;
147
+ }
148
+ const data = await res.json().catch(() => ({}));
149
+ handleError(res.status, data, res.headers);
150
+ }
151
+ return await res.json();
152
+ }
153
+ catch (e) {
154
+ clearTimeout(timer);
155
+ if (e instanceof errors_1.WaterlightError)
156
+ throw e;
157
+ if (e?.name === 'AbortError')
158
+ throw new errors_1.APIError('Request timed out', 408);
159
+ throw new errors_1.APIError(`Network error: ${e?.message ?? e}`, 0);
160
+ }
161
+ }
130
162
  }
131
163
  }
132
164
  exports.Waterlight = Waterlight;
package/dist/errors.d.ts CHANGED
@@ -1,16 +1,18 @@
1
1
  export declare class WaterlightError extends Error {
2
2
  readonly status?: number;
3
- constructor(message: string, status?: number);
3
+ readonly requestId?: string;
4
+ constructor(message: string, status?: number, requestId?: string);
4
5
  }
5
6
  export declare class AuthenticationError extends WaterlightError {
6
- constructor(message: string);
7
+ constructor(message: string, requestId?: string);
7
8
  }
8
9
  export declare class RateLimitError extends WaterlightError {
9
- constructor(message: string);
10
+ readonly retryAfter?: number;
11
+ constructor(message: string, retryAfter?: number, requestId?: string);
10
12
  }
11
13
  export declare class InsufficientCreditsError extends WaterlightError {
12
- constructor(message: string);
14
+ constructor(message: string, requestId?: string);
13
15
  }
14
16
  export declare class APIError extends WaterlightError {
15
- constructor(message: string, status?: number);
17
+ constructor(message: string, status?: number, requestId?: string);
16
18
  }
package/dist/errors.js CHANGED
@@ -2,37 +2,39 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.APIError = exports.InsufficientCreditsError = exports.RateLimitError = exports.AuthenticationError = exports.WaterlightError = void 0;
4
4
  class WaterlightError extends Error {
5
- constructor(message, status) {
5
+ constructor(message, status, requestId) {
6
6
  super(message);
7
7
  this.name = 'WaterlightError';
8
8
  this.status = status;
9
+ this.requestId = requestId;
9
10
  }
10
11
  }
11
12
  exports.WaterlightError = WaterlightError;
12
13
  class AuthenticationError extends WaterlightError {
13
- constructor(message) {
14
- super(message, 401);
14
+ constructor(message, requestId) {
15
+ super(message, 401, requestId);
15
16
  this.name = 'AuthenticationError';
16
17
  }
17
18
  }
18
19
  exports.AuthenticationError = AuthenticationError;
19
20
  class RateLimitError extends WaterlightError {
20
- constructor(message) {
21
- super(message, 429);
21
+ constructor(message, retryAfter, requestId) {
22
+ super(message, 429, requestId);
22
23
  this.name = 'RateLimitError';
24
+ this.retryAfter = retryAfter;
23
25
  }
24
26
  }
25
27
  exports.RateLimitError = RateLimitError;
26
28
  class InsufficientCreditsError extends WaterlightError {
27
- constructor(message) {
28
- super(message, 402);
29
+ constructor(message, requestId) {
30
+ super(message, 402, requestId);
29
31
  this.name = 'InsufficientCreditsError';
30
32
  }
31
33
  }
32
34
  exports.InsufficientCreditsError = InsufficientCreditsError;
33
35
  class APIError extends WaterlightError {
34
- constructor(message, status) {
35
- super(message, status);
36
+ constructor(message, status, requestId) {
37
+ super(message, status, requestId);
36
38
  this.name = 'APIError';
37
39
  }
38
40
  }
@@ -12,6 +12,7 @@ export declare class Stream implements AsyncIterable<ChatCompletionChunk> {
12
12
  private readonly url;
13
13
  private readonly apiKey;
14
14
  private readonly body;
15
- constructor(url: string, apiKey: string, params: object);
15
+ private readonly timeout;
16
+ constructor(url: string, apiKey: string, params: object, timeout?: number);
16
17
  [Symbol.asyncIterator](): AsyncIterator<ChatCompletionChunk>;
17
18
  }
package/dist/streaming.js CHANGED
@@ -12,21 +12,35 @@ const errors_1 = require("./errors");
12
12
  * }
13
13
  */
14
14
  class Stream {
15
- constructor(url, apiKey, params) {
15
+ constructor(url, apiKey, params, timeout = 120000) {
16
16
  this.url = url;
17
17
  this.apiKey = apiKey;
18
18
  this.body = JSON.stringify({ ...params, stream: true });
19
+ this.timeout = timeout;
19
20
  }
20
21
  async *[Symbol.asyncIterator]() {
21
- const res = await fetch(this.url, {
22
- method: 'POST',
23
- headers: {
24
- 'Authorization': `Bearer ${this.apiKey}`,
25
- 'Content-Type': 'application/json',
26
- 'Accept': 'text/event-stream',
27
- },
28
- body: this.body,
29
- });
22
+ const controller = new AbortController();
23
+ const timer = setTimeout(() => controller.abort(), this.timeout);
24
+ let res;
25
+ try {
26
+ res = await fetch(this.url, {
27
+ method: 'POST',
28
+ headers: {
29
+ 'Authorization': `Bearer ${this.apiKey}`,
30
+ 'Content-Type': 'application/json',
31
+ 'Accept': 'text/event-stream',
32
+ 'User-Agent': 'waterlight-node/0.2.0',
33
+ },
34
+ body: this.body,
35
+ signal: controller.signal,
36
+ });
37
+ }
38
+ catch (e) {
39
+ clearTimeout(timer);
40
+ if (e?.name === 'AbortError')
41
+ throw new errors_1.APIError('Stream request timed out', 408);
42
+ throw new errors_1.APIError(`Network error: ${e?.message ?? e}`, 0);
43
+ }
30
44
  if (!res.ok) {
31
45
  let errMsg = '';
32
46
  try {
@@ -66,6 +80,7 @@ class Stream {
66
80
  }
67
81
  }
68
82
  finally {
83
+ clearTimeout(timer);
69
84
  reader.releaseLock();
70
85
  }
71
86
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "waterlight",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "OpenAI-compatible SDK for the Waterlight API",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",