@typesafe-ai/sdk 0.0.0-bootstrap.0 → 0.5.7
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 +34 -7
- package/dist/index.cjs +734 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +415 -0
- package/dist/index.d.mts +415 -0
- package/dist/index.mjs +714 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +70 -8
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,714 @@
|
|
|
1
|
+
const requestIdFrom = (headers) => headers.get("x-typesafe-request-id") ?? void 0;
|
|
2
|
+
/**
|
|
3
|
+
* A promise for the parsed result with access to the HTTP response.
|
|
4
|
+
*
|
|
5
|
+
* Non-2xx responses reject with an `APIError`, including through `asResponse()`.
|
|
6
|
+
*/
|
|
7
|
+
var APIPromise = class APIPromise extends Promise {
|
|
8
|
+
#responsePromise;
|
|
9
|
+
#parseResponse;
|
|
10
|
+
#parsed;
|
|
11
|
+
constructor(responsePromise, parseResponse) {
|
|
12
|
+
super((resolve) => resolve(void 0));
|
|
13
|
+
this.#responsePromise = responsePromise;
|
|
14
|
+
this.#parseResponse = parseResponse;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Resolves to the raw `Response` without parsing the body. SDK requests buffer the full
|
|
18
|
+
* body under the request timeout before handoff; reading it afterwards is caller-owned.
|
|
19
|
+
* The caller owns the body; don't also `await` the parsed result on the same promise.
|
|
20
|
+
*/
|
|
21
|
+
asResponse() {
|
|
22
|
+
return this.#responsePromise;
|
|
23
|
+
}
|
|
24
|
+
/** Return the parsed result, HTTP response, and request ID. */
|
|
25
|
+
async withResponse() {
|
|
26
|
+
const [data, response] = await Promise.all([this.#parse(), this.#responsePromise]);
|
|
27
|
+
return {
|
|
28
|
+
data,
|
|
29
|
+
response,
|
|
30
|
+
requestId: requestIdFrom(response.headers)
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/** Transform the parsed result, sharing the HTTP response and a single body parse. */
|
|
34
|
+
map(fn) {
|
|
35
|
+
return new APIPromise(this.#responsePromise, () => this.#parse().then(fn));
|
|
36
|
+
}
|
|
37
|
+
#parse() {
|
|
38
|
+
this.#parsed ??= this.#responsePromise.then(this.#parseResponse);
|
|
39
|
+
return this.#parsed;
|
|
40
|
+
}
|
|
41
|
+
then(onfulfilled, onrejected) {
|
|
42
|
+
return this.#parse().then(onfulfilled, onrejected);
|
|
43
|
+
}
|
|
44
|
+
catch(onrejected) {
|
|
45
|
+
return this.#parse().catch(onrejected);
|
|
46
|
+
}
|
|
47
|
+
finally(onfinally) {
|
|
48
|
+
return this.#parse().finally(onfinally);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
//#endregion
|
|
52
|
+
//#region src/env.ts
|
|
53
|
+
/** Environment variable names for client configuration. Explicit options take precedence. */
|
|
54
|
+
const ENV = {
|
|
55
|
+
/** Required API key; used when `apiKey` is omitted. */
|
|
56
|
+
apiKey: "TYPESAFE_API_KEY",
|
|
57
|
+
/** API root; defaults to `https://api.typesafe.ai`. */
|
|
58
|
+
baseURL: "TYPESAFE_BASE_URL",
|
|
59
|
+
/** Default model name; defaults to `jev-latest`. */
|
|
60
|
+
defaultModel: "TYPESAFE_DEFAULT_MODEL",
|
|
61
|
+
/** Log level; defaults to `warn`. */
|
|
62
|
+
logLevel: "TYPESAFE_LOG_LEVEL"
|
|
63
|
+
};
|
|
64
|
+
/** Read a trimmed environment value, returning `undefined` for missing or blank values. */
|
|
65
|
+
const readEnv = (name) => {
|
|
66
|
+
if (typeof process === "undefined" || !process.env) return void 0;
|
|
67
|
+
return process.env[name]?.trim() || void 0;
|
|
68
|
+
};
|
|
69
|
+
/** Return the explicit value, falling back to the environment. */
|
|
70
|
+
const fromCodeOrEnv = (fromCode, envVar) => fromCode ?? readEnv(envVar);
|
|
71
|
+
const range = (from, to) => Array.from({ length: to - from }, (_, i) => from + i);
|
|
72
|
+
/** Default SDK retry policy. */
|
|
73
|
+
const DEFAULT_RETRY_POLICY = {
|
|
74
|
+
maxRetries: 2,
|
|
75
|
+
backoffInitialMs: 500,
|
|
76
|
+
backoffMaxMs: 5e3,
|
|
77
|
+
backoffJitter: .25,
|
|
78
|
+
/** HTTP 408, 429, and 5xx responses. */
|
|
79
|
+
httpStatuses: /* @__PURE__ */ new Set([
|
|
80
|
+
408,
|
|
81
|
+
429,
|
|
82
|
+
...range(500, 600)
|
|
83
|
+
]),
|
|
84
|
+
respectRetryAfter: true,
|
|
85
|
+
/** Maximum server retry delay before falling back to backoff. */
|
|
86
|
+
maxRetryAfterMs: 6e4,
|
|
87
|
+
apiConnectionError: true,
|
|
88
|
+
apiTimeoutError: true
|
|
89
|
+
};
|
|
90
|
+
DEFAULT_RETRY_POLICY.maxRetries;
|
|
91
|
+
/** Whether the policy retries an HTTP status code. */
|
|
92
|
+
const isRetryableStatus = (status, policy = DEFAULT_RETRY_POLICY) => policy.httpStatuses.has(status);
|
|
93
|
+
/**
|
|
94
|
+
* Parse `retry-after-ms` or `Retry-After` into milliseconds, preferring `retry-after-ms`.
|
|
95
|
+
*
|
|
96
|
+
* Return `undefined` when neither header contains a valid delay.
|
|
97
|
+
*/
|
|
98
|
+
const parseRetryAfter = (headers, now = Date.now()) => {
|
|
99
|
+
const ms = Number(headers.get("retry-after-ms"));
|
|
100
|
+
if (headers.has("retry-after-ms") && Number.isFinite(ms) && ms >= 0) return ms;
|
|
101
|
+
const raw = headers.get("retry-after");
|
|
102
|
+
if (raw === null) return void 0;
|
|
103
|
+
const seconds = Number(raw);
|
|
104
|
+
if (Number.isFinite(seconds)) return seconds >= 0 ? seconds * 1e3 : void 0;
|
|
105
|
+
const date = Date.parse(raw);
|
|
106
|
+
if (!Number.isNaN(date)) return Math.max(0, date - now);
|
|
107
|
+
};
|
|
108
|
+
/**
|
|
109
|
+
* Calculate the delay in milliseconds for a zero-based retry attempt.
|
|
110
|
+
*
|
|
111
|
+
* Use an allowed server delay; otherwise use capped exponential backoff with jitter.
|
|
112
|
+
*/
|
|
113
|
+
const retryDelayMs = (attempt, headers, policy = DEFAULT_RETRY_POLICY, random = Math.random) => {
|
|
114
|
+
if (policy.respectRetryAfter && headers !== void 0) {
|
|
115
|
+
const retryAfter = parseRetryAfter(headers);
|
|
116
|
+
if (retryAfter !== void 0 && retryAfter <= policy.maxRetryAfterMs) return retryAfter;
|
|
117
|
+
}
|
|
118
|
+
const exponential = Math.min(policy.backoffInitialMs * 2 ** attempt, policy.backoffMaxMs);
|
|
119
|
+
return Math.round(exponential * (1 - random() * policy.backoffJitter));
|
|
120
|
+
};
|
|
121
|
+
/** Wait `ms` milliseconds, rejecting with `signal.reason` on cancellation. */
|
|
122
|
+
const sleep = (ms, signal) => new Promise((resolve, reject) => {
|
|
123
|
+
if (signal?.aborted) return reject(signal.reason);
|
|
124
|
+
const onAbort = () => {
|
|
125
|
+
clearTimeout(timer);
|
|
126
|
+
reject(signal?.reason);
|
|
127
|
+
};
|
|
128
|
+
const timer = setTimeout(() => {
|
|
129
|
+
signal?.removeEventListener("abort", onAbort);
|
|
130
|
+
resolve();
|
|
131
|
+
}, ms);
|
|
132
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
133
|
+
});
|
|
134
|
+
//#endregion
|
|
135
|
+
//#region src/errors.ts
|
|
136
|
+
/** Base class for SDK errors. */
|
|
137
|
+
var TypeSafeError = class extends Error {
|
|
138
|
+
constructor(message, options) {
|
|
139
|
+
super(message, options);
|
|
140
|
+
this.name = new.target.name;
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
const isRecord = (value) => typeof value === "object" && value !== null;
|
|
144
|
+
/** Extract a message from a text, error, or validation response body. */
|
|
145
|
+
const extractMessage = (body) => {
|
|
146
|
+
if (typeof body === "string") return body || void 0;
|
|
147
|
+
if (!isRecord(body)) return void 0;
|
|
148
|
+
const { error, message, detail } = body;
|
|
149
|
+
if (typeof error === "string") return error;
|
|
150
|
+
if (isRecord(error) && typeof error.message === "string") return error.message;
|
|
151
|
+
if (typeof message === "string") return message;
|
|
152
|
+
if (typeof detail === "string") return detail;
|
|
153
|
+
if (isRecord(detail) && typeof detail.message === "string") return detail.message;
|
|
154
|
+
if (Array.isArray(detail)) return describeValidationErrors(detail);
|
|
155
|
+
};
|
|
156
|
+
/** Format validation errors as semicolon-separated `path: message` entries. */
|
|
157
|
+
const describeValidationErrors = (errors) => {
|
|
158
|
+
const parts = errors.flatMap((e) => {
|
|
159
|
+
if (!isRecord(e) || typeof e.msg !== "string") return [];
|
|
160
|
+
const loc = Array.isArray(e.loc) ? e.loc.filter((x) => x !== "body").join(".") : "";
|
|
161
|
+
return [loc ? `${loc}: ${e.msg}` : e.msg];
|
|
162
|
+
});
|
|
163
|
+
return parts.length > 0 ? parts.join("; ") : void 0;
|
|
164
|
+
};
|
|
165
|
+
const MAX_RAW_BODY_IN_MESSAGE = 200;
|
|
166
|
+
/** An unsuccessful HTTP response from the API. */
|
|
167
|
+
var APIError = class APIError extends TypeSafeError {
|
|
168
|
+
/** HTTP response status code. */
|
|
169
|
+
status;
|
|
170
|
+
/** HTTP response headers. */
|
|
171
|
+
headers;
|
|
172
|
+
/** Parsed JSON, response text, or `undefined` for an empty body. */
|
|
173
|
+
body;
|
|
174
|
+
/** Request ID from `x-typesafe-request-id`, or `undefined` when absent. */
|
|
175
|
+
requestId;
|
|
176
|
+
constructor(status, body, headers, message) {
|
|
177
|
+
super(message ?? APIError.describe(status, body));
|
|
178
|
+
this.status = status;
|
|
179
|
+
this.body = body;
|
|
180
|
+
this.headers = headers;
|
|
181
|
+
this.requestId = requestIdFrom(headers);
|
|
182
|
+
}
|
|
183
|
+
static describe(status, body) {
|
|
184
|
+
const detail = extractMessage(body);
|
|
185
|
+
if (detail) return `${status} ${detail}`;
|
|
186
|
+
if (body === void 0) return `${status} status code (no body)`;
|
|
187
|
+
const raw = typeof body === "string" ? body : JSON.stringify(body);
|
|
188
|
+
return `${status} ${raw.length > MAX_RAW_BODY_IN_MESSAGE ? `${raw.slice(0, MAX_RAW_BODY_IN_MESSAGE)}…` : raw}`;
|
|
189
|
+
}
|
|
190
|
+
/** Create the error subclass for an HTTP status code. */
|
|
191
|
+
static fromResponse(status, body, headers) {
|
|
192
|
+
if (status === 400) return new BadRequestError(status, body, headers);
|
|
193
|
+
if (status === 401) return new AuthenticationError(status, body, headers);
|
|
194
|
+
if (status === 403) return new PermissionDeniedError(status, body, headers);
|
|
195
|
+
if (status === 404) return new NotFoundError(status, body, headers);
|
|
196
|
+
if (status === 422) return new UnprocessableEntityError(status, body, headers);
|
|
197
|
+
if (status === 429) return new RateLimitError(status, body, headers);
|
|
198
|
+
if (status >= 500) return new InternalServerError(status, body, headers);
|
|
199
|
+
return new APIError(status, body, headers);
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
/** HTTP 400: the request is invalid. */
|
|
203
|
+
var BadRequestError = class extends APIError {};
|
|
204
|
+
/** HTTP 401: authentication failed. */
|
|
205
|
+
var AuthenticationError = class extends APIError {};
|
|
206
|
+
/** HTTP 403: access is denied. */
|
|
207
|
+
var PermissionDeniedError = class extends APIError {};
|
|
208
|
+
/** HTTP 404: the resource was not found. */
|
|
209
|
+
var NotFoundError = class extends APIError {};
|
|
210
|
+
/** HTTP 422: request validation failed. */
|
|
211
|
+
var UnprocessableEntityError = class extends APIError {};
|
|
212
|
+
/** HTTP 429: the rate limit was exceeded. */
|
|
213
|
+
var RateLimitError = class extends APIError {
|
|
214
|
+
/** Server retry delay in milliseconds, or `undefined` when absent or invalid. */
|
|
215
|
+
retryAfterMs = parseRetryAfter(this.headers);
|
|
216
|
+
};
|
|
217
|
+
/** HTTP 5xx: the server failed to handle the request. */
|
|
218
|
+
var InternalServerError = class extends APIError {};
|
|
219
|
+
/** The request or response-body delivery failed (DNS, TLS, connection closed, etc.). */
|
|
220
|
+
var APIConnectionError = class extends TypeSafeError {
|
|
221
|
+
constructor(message = "Connection error.", options) {
|
|
222
|
+
super(message, options);
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
/** The full response did not arrive within the timeout. A kind of `APIConnectionError`. */
|
|
226
|
+
var APITimeoutError = class extends APIConnectionError {
|
|
227
|
+
/** Configured timeout in milliseconds. */
|
|
228
|
+
timeoutMs;
|
|
229
|
+
constructor(timeoutMs, options) {
|
|
230
|
+
super(`Request timed out after ${timeoutMs}ms.`, options);
|
|
231
|
+
this.timeoutMs = timeoutMs;
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
/** The caller cancelled the request through an `AbortSignal`. */
|
|
235
|
+
var APIUserAbortError = class extends TypeSafeError {
|
|
236
|
+
constructor(message = "Request was aborted.", options) {
|
|
237
|
+
super(message, options);
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
//#endregion
|
|
241
|
+
//#region src/logging.ts
|
|
242
|
+
/** Supported log levels, from most to least verbose. */
|
|
243
|
+
const LOG_LEVELS = [
|
|
244
|
+
"debug",
|
|
245
|
+
"info",
|
|
246
|
+
"warn",
|
|
247
|
+
"error",
|
|
248
|
+
"off"
|
|
249
|
+
];
|
|
250
|
+
const DEFAULT_LOG_LEVEL = "warn";
|
|
251
|
+
const isLogLevel = (value) => LOG_LEVELS.includes(value);
|
|
252
|
+
/** Validate a configured log level, throwing `TypeSafeError` for unknown values. */
|
|
253
|
+
const parseLogLevel = (value, source) => {
|
|
254
|
+
if (isLogLevel(value)) return value;
|
|
255
|
+
throw new TypeSafeError(`Invalid log level "${value}" from ${source}. Expected one of: ${LOG_LEVELS.join(", ")}.`);
|
|
256
|
+
};
|
|
257
|
+
const PREFIX = "[typesafe-sdk]";
|
|
258
|
+
/** Default console logger with the `[typesafe-sdk]` prefix. */
|
|
259
|
+
const consoleLogger = {
|
|
260
|
+
debug: (message, ...args) => console.debug(`${PREFIX} ${message}`, ...args),
|
|
261
|
+
info: (message, ...args) => console.info(`${PREFIX} ${message}`, ...args),
|
|
262
|
+
warn: (message, ...args) => console.warn(`${PREFIX} ${message}`, ...args),
|
|
263
|
+
error: (message, ...args) => console.error(`${PREFIX} ${message}`, ...args)
|
|
264
|
+
};
|
|
265
|
+
const RANK = {
|
|
266
|
+
debug: 0,
|
|
267
|
+
info: 1,
|
|
268
|
+
warn: 2,
|
|
269
|
+
error: 3,
|
|
270
|
+
off: 4
|
|
271
|
+
};
|
|
272
|
+
const drop = () => {};
|
|
273
|
+
/** Filter logger calls to the configured level and above. */
|
|
274
|
+
const withLevel = (sink, level) => {
|
|
275
|
+
const enabled = (at) => RANK[at] >= RANK[level];
|
|
276
|
+
return {
|
|
277
|
+
debug: enabled("debug") ? (message, ...args) => sink.debug(message, ...args) : drop,
|
|
278
|
+
info: enabled("info") ? (message, ...args) => sink.info(message, ...args) : drop,
|
|
279
|
+
warn: enabled("warn") ? (message, ...args) => sink.warn(message, ...args) : drop,
|
|
280
|
+
error: enabled("error") ? (message, ...args) => sink.error(message, ...args) : drop
|
|
281
|
+
};
|
|
282
|
+
};
|
|
283
|
+
/** Credential headers that retain a key suffix for identification. */
|
|
284
|
+
const KEY_HEADERS = /* @__PURE__ */ new Set([
|
|
285
|
+
"authorization",
|
|
286
|
+
"proxy-authorization",
|
|
287
|
+
"x-api-key"
|
|
288
|
+
]);
|
|
289
|
+
/** Headers whose values are redacted in full. */
|
|
290
|
+
const OPAQUE_HEADERS = /* @__PURE__ */ new Set(["cookie", "set-cookie"]);
|
|
291
|
+
/** Mask a key, preserving its scheme and the last four characters of secrets longer than eight. */
|
|
292
|
+
const redactKey = (value) => {
|
|
293
|
+
const [scheme, secret] = value.includes(" ") ? value.split(/\s+/, 2) : [void 0, value];
|
|
294
|
+
const tail = secret && secret.length > 8 ? secret.slice(-4) : "";
|
|
295
|
+
return `${scheme ? `${scheme} ` : ""}***${tail}`;
|
|
296
|
+
};
|
|
297
|
+
const redact = (name, value) => {
|
|
298
|
+
const lower = name.toLowerCase();
|
|
299
|
+
if (KEY_HEADERS.has(lower)) return redactKey(value);
|
|
300
|
+
if (OPAQUE_HEADERS.has(lower)) return "***";
|
|
301
|
+
return value;
|
|
302
|
+
};
|
|
303
|
+
/** Copy headers with known credential values redacted. */
|
|
304
|
+
const redactHeaders = (headers) => Object.fromEntries(Object.entries(headers).map(([name, value]) => [name, redact(name, value)]));
|
|
305
|
+
//#endregion
|
|
306
|
+
//#region src/questions.ts
|
|
307
|
+
/**
|
|
308
|
+
* Create a yes/no question with optional descriptions for either outcome.
|
|
309
|
+
*
|
|
310
|
+
* @param instructions - The question as text, a JSON object or array; defaults to `null`.
|
|
311
|
+
* @param criteria - Optional descriptions of the yes and no outcomes.
|
|
312
|
+
*/
|
|
313
|
+
const noul = (instructions = null, criteria) => ({
|
|
314
|
+
type: "noul",
|
|
315
|
+
instructions,
|
|
316
|
+
criteria
|
|
317
|
+
});
|
|
318
|
+
/**
|
|
319
|
+
* Create a score question using an ordered rubric.
|
|
320
|
+
*
|
|
321
|
+
* @param instructions - The question as text, a JSON object or array, or `null`.
|
|
322
|
+
* @param criteria - A nonempty array or map indexed from zero with no gaps; descriptions may be `null`.
|
|
323
|
+
*/
|
|
324
|
+
const score = (instructions, criteria) => ({
|
|
325
|
+
type: "score",
|
|
326
|
+
instructions,
|
|
327
|
+
criteria
|
|
328
|
+
});
|
|
329
|
+
/**
|
|
330
|
+
* Create a question that selects between named alternatives.
|
|
331
|
+
*
|
|
332
|
+
* @param instructions - The question as text, a JSON object or array, or `null`.
|
|
333
|
+
* @param criteria - Labels mapped to descriptions, or `null` for undescribed labels.
|
|
334
|
+
*/
|
|
335
|
+
const choice = (instructions, criteria) => {
|
|
336
|
+
if (Array.isArray(criteria)) throw new TypeSafeError("Choice criteria must be a map of labels to descriptions, not a list.");
|
|
337
|
+
return {
|
|
338
|
+
type: "choice",
|
|
339
|
+
instructions,
|
|
340
|
+
criteria
|
|
341
|
+
};
|
|
342
|
+
};
|
|
343
|
+
const isScoreList = (criteria) => Array.isArray(criteria);
|
|
344
|
+
/** Validate score keys and convert the map to a nonempty array indexed from zero. */
|
|
345
|
+
const scoreMapToList = (map, name) => {
|
|
346
|
+
const keys = Object.keys(map).map((key) => {
|
|
347
|
+
const n = Number(key);
|
|
348
|
+
if (!Number.isInteger(n) || n < 0) throw new TypeSafeError(`Score question "${name}" has criteria key "${key}"; keys must be non-negative integers.`);
|
|
349
|
+
return n;
|
|
350
|
+
}).sort((a, b) => a - b);
|
|
351
|
+
if (keys.length === 0) throw noScores(name);
|
|
352
|
+
const expected = keys.map((_, i) => i);
|
|
353
|
+
if (keys.some((k, i) => k !== expected[i])) throw new TypeSafeError(`Score question "${name}" defines scores ${keys.join(", ")}, but scores must run from 0 with no gaps (expected ${expected.join(", ")}).`);
|
|
354
|
+
return keys.map((k) => map[k]);
|
|
355
|
+
};
|
|
356
|
+
const noScores = (name) => new TypeSafeError(`Score question "${name}" has no criteria; at least one score is required.`);
|
|
357
|
+
/** Validate nonempty questions and score criteria, converting score maps to arrays. */
|
|
358
|
+
const toWireQuestions = (questions) => {
|
|
359
|
+
if (Object.keys(questions).length === 0) throw new TypeSafeError("At least one question is required.");
|
|
360
|
+
let changed = false;
|
|
361
|
+
const wire = Object.create(null);
|
|
362
|
+
for (const [name, question] of Object.entries(questions)) if (question.type !== "score") wire[name] = question;
|
|
363
|
+
else if (isScoreList(question.criteria)) {
|
|
364
|
+
if (question.criteria.length === 0) throw noScores(name);
|
|
365
|
+
wire[name] = question;
|
|
366
|
+
} else {
|
|
367
|
+
wire[name] = {
|
|
368
|
+
...question,
|
|
369
|
+
criteria: scoreMapToList(question.criteria, name)
|
|
370
|
+
};
|
|
371
|
+
changed = true;
|
|
372
|
+
}
|
|
373
|
+
return changed ? wire : questions;
|
|
374
|
+
};
|
|
375
|
+
//#endregion
|
|
376
|
+
//#region src/resources/models.ts
|
|
377
|
+
/** Access to the Models API resource. */
|
|
378
|
+
var Models = class {
|
|
379
|
+
#transport;
|
|
380
|
+
constructor(transport) {
|
|
381
|
+
this.#transport = transport;
|
|
382
|
+
}
|
|
383
|
+
/** List the models available to the account. */
|
|
384
|
+
list(options = {}) {
|
|
385
|
+
return this.#transport.request("GET", "/v1/models", options).map(unwrapModels);
|
|
386
|
+
}
|
|
387
|
+
};
|
|
388
|
+
const unwrapModels = (wire) => {
|
|
389
|
+
if (Array.isArray(wire?.models)) return wire.models;
|
|
390
|
+
throw new TypeSafeError("Unexpected response shape from GET /v1/models; expected { models: [...] }.");
|
|
391
|
+
};
|
|
392
|
+
//#endregion
|
|
393
|
+
//#region src/runtime.ts
|
|
394
|
+
const g = globalThis;
|
|
395
|
+
/** Whether browser page globals are present. */
|
|
396
|
+
const isBrowser = () => typeof g.window !== "undefined" && typeof g.window.document !== "undefined" && typeof g.navigator !== "undefined";
|
|
397
|
+
/** Runtime name, version, and platform for the `X-TypeSafe-Runtime` header. */
|
|
398
|
+
const describeRuntime = () => {
|
|
399
|
+
const platform = g.process?.platform && g.process?.arch ? ` (${g.process.platform}; ${g.process.arch})` : "";
|
|
400
|
+
if (g.Bun?.version) return `bun/${g.Bun.version}${platform}`;
|
|
401
|
+
if (g.Deno?.version?.deno) return `deno/${g.Deno.version.deno}${platform}`;
|
|
402
|
+
if (g.EdgeRuntime !== void 0) return "vercel-edge";
|
|
403
|
+
if (g.navigator?.userAgent === "Cloudflare-Workers") return "cloudflare-workers";
|
|
404
|
+
if (g.process?.versions?.node) return `node/${g.process.versions.node}${platform}`;
|
|
405
|
+
if (isBrowser()) return "browser";
|
|
406
|
+
return "unknown";
|
|
407
|
+
};
|
|
408
|
+
//#endregion
|
|
409
|
+
//#region src/version.ts
|
|
410
|
+
const VERSION = "0.5.7";
|
|
411
|
+
const missingApiKey = () => {
|
|
412
|
+
throw new TypeSafeError(`No API key was provided. Pass \`apiKey\` to the TypeSafeClient constructor or set the ${ENV.apiKey} environment variable.`);
|
|
413
|
+
};
|
|
414
|
+
const missingFetch = () => {
|
|
415
|
+
throw new TypeSafeError("No global `fetch` is available in this runtime. Pass a `fetch` implementation to the TypeSafeClient constructor.");
|
|
416
|
+
};
|
|
417
|
+
const refuseBrowser = () => {
|
|
418
|
+
throw new TypeSafeError("TypeSafeClient is running in a browser, which would expose your API key to anyone using the page. Call the API from a server instead, or pass `dangerouslyAllowBrowser: true` if you understand the risk.");
|
|
419
|
+
};
|
|
420
|
+
/** Call global `fetch` with its required receiver in browsers. */
|
|
421
|
+
const defaultFetch = (input, init) => globalThis.fetch(input, init);
|
|
422
|
+
const assertNonNegativeInteger = (name, value) => {
|
|
423
|
+
if (!Number.isInteger(value) || value < 0) throw new TypeSafeError(`\`${name}\` must be a non-negative integer, got ${String(value)}.`);
|
|
424
|
+
return value;
|
|
425
|
+
};
|
|
426
|
+
const assertPositiveMs = (name, value) => {
|
|
427
|
+
if (!Number.isFinite(value) || value <= 0) throw new TypeSafeError(`\`${name}\` must be a positive number of milliseconds, got ${String(value)}.`);
|
|
428
|
+
return value;
|
|
429
|
+
};
|
|
430
|
+
const assertNonNegativeMs = (name, value) => {
|
|
431
|
+
if (!Number.isFinite(value) || value < 0) throw new TypeSafeError(`\`${name}\` must be a non-negative number of milliseconds, got ${String(value)}.`);
|
|
432
|
+
return value;
|
|
433
|
+
};
|
|
434
|
+
const assertFraction = (name, value) => {
|
|
435
|
+
if (!Number.isFinite(value) || value < 0 || value > 1) throw new TypeSafeError(`\`${name}\` must be between 0 and 1, got ${String(value)}.`);
|
|
436
|
+
return value;
|
|
437
|
+
};
|
|
438
|
+
const assertStatusSet = (name, statuses) => {
|
|
439
|
+
for (const status of statuses) if (!Number.isInteger(status) || status < 100 || status > 999) throw new TypeSafeError(`\`${name}\` must contain HTTP status codes, got ${String(status)}.`);
|
|
440
|
+
return statuses;
|
|
441
|
+
};
|
|
442
|
+
/** Merge and validate retry overrides, copying the status set to isolate later mutations. */
|
|
443
|
+
const resolveRetryPolicy = (base, overrides) => {
|
|
444
|
+
const o = overrides ?? {};
|
|
445
|
+
return {
|
|
446
|
+
maxRetries: o.maxRetries === void 0 ? base.maxRetries : assertNonNegativeInteger("retry.maxRetries", o.maxRetries),
|
|
447
|
+
backoffInitialMs: o.backoffInitialMs === void 0 ? base.backoffInitialMs : assertNonNegativeMs("retry.backoffInitialMs", o.backoffInitialMs),
|
|
448
|
+
backoffMaxMs: o.backoffMaxMs === void 0 ? base.backoffMaxMs : assertNonNegativeMs("retry.backoffMaxMs", o.backoffMaxMs),
|
|
449
|
+
backoffJitter: o.backoffJitter === void 0 ? base.backoffJitter : assertFraction("retry.backoffJitter", o.backoffJitter),
|
|
450
|
+
httpStatuses: new Set(o.httpStatuses === void 0 ? base.httpStatuses : assertStatusSet("retry.httpStatuses", o.httpStatuses)),
|
|
451
|
+
respectRetryAfter: o.respectRetryAfter ?? base.respectRetryAfter,
|
|
452
|
+
maxRetryAfterMs: o.maxRetryAfterMs === void 0 ? base.maxRetryAfterMs : assertNonNegativeMs("retry.maxRetryAfterMs", o.maxRetryAfterMs),
|
|
453
|
+
apiConnectionError: o.apiConnectionError ?? base.apiConnectionError,
|
|
454
|
+
apiTimeoutError: o.apiTimeoutError ?? base.apiTimeoutError
|
|
455
|
+
};
|
|
456
|
+
};
|
|
457
|
+
/** Whether the policy retries a connection error or timeout. */
|
|
458
|
+
const isRetryableError = (err, policy) => {
|
|
459
|
+
if (err instanceof APITimeoutError) return policy.apiTimeoutError;
|
|
460
|
+
if (err instanceof APIConnectionError) return policy.apiConnectionError;
|
|
461
|
+
return false;
|
|
462
|
+
};
|
|
463
|
+
/** Resolve and validate the log level from configuration or the environment. */
|
|
464
|
+
const resolveLogLevel = (fromCode) => {
|
|
465
|
+
if (fromCode !== void 0) return parseLogLevel(fromCode, "the `logLevel` option");
|
|
466
|
+
const fromEnv = readEnv(ENV.logLevel);
|
|
467
|
+
if (fromEnv !== void 0) return parseLogLevel(fromEnv, ENV.logLevel);
|
|
468
|
+
return DEFAULT_LOG_LEVEL;
|
|
469
|
+
};
|
|
470
|
+
const stripTrailingSlashes = (url) => url.replace(/\/+$/, "");
|
|
471
|
+
/** Last value wins regardless of casing; undefined removes a protected header. */
|
|
472
|
+
const mergeHeaders = (...sources) => {
|
|
473
|
+
const entries = /* @__PURE__ */ new Map();
|
|
474
|
+
for (const source of sources) for (const [name, value] of Object.entries(source)) if (value === void 0) entries.delete(name.toLowerCase());
|
|
475
|
+
else entries.set(name.toLowerCase(), [name, value]);
|
|
476
|
+
return Object.fromEntries(entries.values());
|
|
477
|
+
};
|
|
478
|
+
/** Drain a clone so the original response retains its metadata and a readable, buffered body. */
|
|
479
|
+
const bufferResponse = async (response, signal) => {
|
|
480
|
+
const reader = response.clone().body?.getReader();
|
|
481
|
+
if (!reader) return;
|
|
482
|
+
const cancel = () => {
|
|
483
|
+
reader.cancel(signal.reason).catch(() => {});
|
|
484
|
+
response.body?.cancel(signal.reason).catch(() => {});
|
|
485
|
+
};
|
|
486
|
+
signal.addEventListener("abort", cancel, { once: true });
|
|
487
|
+
try {
|
|
488
|
+
if (signal.aborted) cancel();
|
|
489
|
+
signal.throwIfAborted();
|
|
490
|
+
while (!(await reader.read()).done) signal.throwIfAborted();
|
|
491
|
+
signal.throwIfAborted();
|
|
492
|
+
} finally {
|
|
493
|
+
signal.removeEventListener("abort", cancel);
|
|
494
|
+
reader.releaseLock();
|
|
495
|
+
}
|
|
496
|
+
};
|
|
497
|
+
/** Runtime description cached for the process lifetime. */
|
|
498
|
+
const RUNTIME = describeRuntime();
|
|
499
|
+
/** Client for the TypeSafe AI API. */
|
|
500
|
+
var TypeSafeClient = class {
|
|
501
|
+
/** API key excluded from serialization and public properties. */
|
|
502
|
+
#apiKey;
|
|
503
|
+
/** API root with trailing slashes removed. */
|
|
504
|
+
baseURL;
|
|
505
|
+
/** Model used when a request omits `model`. */
|
|
506
|
+
defaultModel;
|
|
507
|
+
/** Configured log verbosity. */
|
|
508
|
+
logLevel;
|
|
509
|
+
/** The configured logger, filtered to `logLevel`. */
|
|
510
|
+
logger;
|
|
511
|
+
/** Retry settings with constructor overrides applied. */
|
|
512
|
+
retry;
|
|
513
|
+
/** Timeout per attempt in milliseconds. */
|
|
514
|
+
timeout;
|
|
515
|
+
/** Additional headers sent with each request. */
|
|
516
|
+
defaultHeaders;
|
|
517
|
+
/** HTTP fetch implementation. */
|
|
518
|
+
fetch;
|
|
519
|
+
/** The models available to the account. */
|
|
520
|
+
models;
|
|
521
|
+
#requestCount = 0;
|
|
522
|
+
/**
|
|
523
|
+
* Create a client for the TypeSafe AI API.
|
|
524
|
+
*
|
|
525
|
+
* Explicit options take precedence over environment variables, then SDK defaults.
|
|
526
|
+
* Empty or whitespace-only environment values are ignored.
|
|
527
|
+
*
|
|
528
|
+
* @throws {TypeSafeError} The API key is missing, configuration is invalid, or the runtime is unsupported.
|
|
529
|
+
*/
|
|
530
|
+
constructor(config = {}) {
|
|
531
|
+
if (isBrowser() && !config.dangerouslyAllowBrowser) refuseBrowser();
|
|
532
|
+
this.#apiKey = fromCodeOrEnv(config.apiKey, ENV.apiKey) ?? missingApiKey();
|
|
533
|
+
this.baseURL = stripTrailingSlashes(fromCodeOrEnv(config.baseURL, ENV.baseURL) ?? "https://api.typesafe.ai");
|
|
534
|
+
this.defaultModel = fromCodeOrEnv(config.defaultModel, ENV.defaultModel) ?? "jev-latest";
|
|
535
|
+
this.logLevel = resolveLogLevel(config.logLevel);
|
|
536
|
+
this.logger = withLevel(config.logger ?? consoleLogger, this.logLevel);
|
|
537
|
+
this.retry = resolveRetryPolicy(DEFAULT_RETRY_POLICY, config.retry);
|
|
538
|
+
this.timeout = assertPositiveMs("timeout", config.timeout ?? 1e4);
|
|
539
|
+
this.defaultHeaders = { ...config.defaultHeaders };
|
|
540
|
+
if (config.fetch === void 0 && typeof globalThis.fetch !== "function") missingFetch();
|
|
541
|
+
this.fetch = config.fetch ?? defaultFetch;
|
|
542
|
+
const transport = {
|
|
543
|
+
request: (method, path, options) => this.#request(method, path, options),
|
|
544
|
+
defaultModel: this.defaultModel
|
|
545
|
+
};
|
|
546
|
+
this.models = new Models(transport);
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* Answer named questions about text or structured state.
|
|
550
|
+
*
|
|
551
|
+
* @param request - State, questions, and an optional model override.
|
|
552
|
+
* @param options - Per-call timeout, retry, headers, and cancellation settings.
|
|
553
|
+
* @returns Answers typed by question name and criteria, with model and token usage.
|
|
554
|
+
* @throws {TypeSafeError} Questions or score criteria are empty, or score keys are invalid.
|
|
555
|
+
* @throws {APIError} The server returns a non-2xx response after retries.
|
|
556
|
+
* @throws {APIConnectionError} The request cannot connect or times out after retries.
|
|
557
|
+
* @throws {APIUserAbortError} The caller aborts the request.
|
|
558
|
+
*
|
|
559
|
+
* @example
|
|
560
|
+
* ```ts
|
|
561
|
+
* const { answers } = await client.systemOne({
|
|
562
|
+
* state: "I was charged twice. Please help.",
|
|
563
|
+
* questions: { billing: noul("Is this about billing?") },
|
|
564
|
+
* });
|
|
565
|
+
* console.log(answers.billing.noul);
|
|
566
|
+
* ```
|
|
567
|
+
*/
|
|
568
|
+
systemOne(request, options = {}) {
|
|
569
|
+
const body = {
|
|
570
|
+
...request,
|
|
571
|
+
model: request.model ?? this.defaultModel,
|
|
572
|
+
questions: toWireQuestions(request.questions)
|
|
573
|
+
};
|
|
574
|
+
return this.#request("POST", "/v1/systemone", {
|
|
575
|
+
...options,
|
|
576
|
+
body
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
/** Send a request and parse its response body. */
|
|
580
|
+
#request(method, path, options = {}) {
|
|
581
|
+
const resolved = {
|
|
582
|
+
method,
|
|
583
|
+
path,
|
|
584
|
+
body: options.body,
|
|
585
|
+
headers: mergeHeaders(this.defaultHeaders, options.headers ?? {}),
|
|
586
|
+
signal: options.signal,
|
|
587
|
+
timeout: options.timeout === void 0 ? this.timeout : assertPositiveMs("timeout", options.timeout),
|
|
588
|
+
retry: resolveRetryPolicy(this.retry, options.retry)
|
|
589
|
+
};
|
|
590
|
+
const tag = `#${++this.#requestCount} ${method} ${path}`;
|
|
591
|
+
return new APIPromise(this.fetchWithRetries(tag, resolved), async (res) => {
|
|
592
|
+
const parsed = await parseBody(res);
|
|
593
|
+
this.logger.debug(`${tag} <- body`, parsed);
|
|
594
|
+
return parsed;
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
/** Retry eligible failures, logging attempt summaries at `info` and headers and bodies at `debug`. */
|
|
598
|
+
async fetchWithRetries(tag, req) {
|
|
599
|
+
const url = `${this.baseURL}${req.path}`;
|
|
600
|
+
const headers = mergeHeaders(req.headers, {
|
|
601
|
+
Authorization: `Bearer ${this.#apiKey}`,
|
|
602
|
+
Accept: "application/json",
|
|
603
|
+
"User-Agent": `typesafe-sdk/${VERSION}`,
|
|
604
|
+
"X-TypeSafe-SDK": `typesafe-sdk/${VERSION}`,
|
|
605
|
+
"X-TypeSafe-Runtime": RUNTIME,
|
|
606
|
+
"Content-Type": req.body === void 0 ? void 0 : "application/json",
|
|
607
|
+
"X-TypeSafe-Retry-Count": void 0
|
|
608
|
+
});
|
|
609
|
+
const body = req.body === void 0 ? void 0 : JSON.stringify(req.body);
|
|
610
|
+
for (let attempt = 0;; attempt++) {
|
|
611
|
+
const retriesLeft = req.retry.maxRetries - attempt;
|
|
612
|
+
const attemptHeaders = attempt === 0 ? headers : {
|
|
613
|
+
...headers,
|
|
614
|
+
"X-TypeSafe-Retry-Count": String(attempt)
|
|
615
|
+
};
|
|
616
|
+
this.logger.debug(`${tag} -> ${url}`, {
|
|
617
|
+
headers: redactHeaders(attemptHeaders),
|
|
618
|
+
body: req.body
|
|
619
|
+
});
|
|
620
|
+
const started = Date.now();
|
|
621
|
+
let res;
|
|
622
|
+
try {
|
|
623
|
+
res = await this.attempt(tag, url, {
|
|
624
|
+
method: req.method,
|
|
625
|
+
headers: attemptHeaders,
|
|
626
|
+
body
|
|
627
|
+
}, req);
|
|
628
|
+
} catch (err) {
|
|
629
|
+
if (err instanceof APIUserAbortError || retriesLeft <= 0) throw err;
|
|
630
|
+
if (!isRetryableError(err, req.retry)) throw err;
|
|
631
|
+
await this.backOff(tag, attempt, retriesLeft, err.message, void 0, req);
|
|
632
|
+
continue;
|
|
633
|
+
}
|
|
634
|
+
const requestId = requestIdFrom(res.headers);
|
|
635
|
+
this.logger.info(`${tag} <- ${res.status} in ${Date.now() - started}ms${requestId ? ` (request ${requestId})` : ""}`);
|
|
636
|
+
if (res.ok) return res;
|
|
637
|
+
const errorBody = await parseBody(res);
|
|
638
|
+
this.logger.debug(`${tag} <- error body`, errorBody);
|
|
639
|
+
const error = APIError.fromResponse(res.status, errorBody, res.headers);
|
|
640
|
+
if (retriesLeft <= 0 || !isRetryableStatus(res.status, req.retry)) throw error;
|
|
641
|
+
await this.backOff(tag, attempt, retriesLeft, `${res.status}`, res.headers, req);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
/**
|
|
645
|
+
* One HTTP round trip, including body delivery, with a timeout. The caller's signal and our
|
|
646
|
+
* timer both abort the same controller; we check which fired to choose the error class.
|
|
647
|
+
*/
|
|
648
|
+
async attempt(tag, url, init, { signal, timeout }) {
|
|
649
|
+
const controller = new AbortController();
|
|
650
|
+
const abortFromCaller = () => controller.abort(signal?.reason);
|
|
651
|
+
if (signal?.aborted) abortFromCaller();
|
|
652
|
+
signal?.addEventListener("abort", abortFromCaller, { once: true });
|
|
653
|
+
let timedOut = false;
|
|
654
|
+
const timer = setTimeout(() => {
|
|
655
|
+
timedOut = true;
|
|
656
|
+
controller.abort();
|
|
657
|
+
}, timeout);
|
|
658
|
+
const started = Date.now();
|
|
659
|
+
const elapsed = () => `${Date.now() - started}ms`;
|
|
660
|
+
try {
|
|
661
|
+
const response = await this.fetch(url, {
|
|
662
|
+
...init,
|
|
663
|
+
signal: controller.signal
|
|
664
|
+
});
|
|
665
|
+
await bufferResponse(response, controller.signal);
|
|
666
|
+
return response;
|
|
667
|
+
} catch (err) {
|
|
668
|
+
if (signal?.aborted) {
|
|
669
|
+
this.logger.info(`${tag} aborted by caller after ${elapsed()}`);
|
|
670
|
+
throw new APIUserAbortError(void 0, { cause: err });
|
|
671
|
+
}
|
|
672
|
+
if (timedOut) {
|
|
673
|
+
this.logger.info(`${tag} timed out after ${elapsed()}`);
|
|
674
|
+
throw new APITimeoutError(timeout, { cause: err });
|
|
675
|
+
}
|
|
676
|
+
this.logger.info(`${tag} connection error after ${elapsed()}`, err);
|
|
677
|
+
throw new APIConnectionError(err instanceof Error ? `Connection error: ${err.message}` : void 0, { cause: err });
|
|
678
|
+
} finally {
|
|
679
|
+
clearTimeout(timer);
|
|
680
|
+
signal?.removeEventListener("abort", abortFromCaller);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
/** Wait before retrying; caller cancellation throws `APIUserAbortError`. */
|
|
684
|
+
async backOff(tag, attempt, retriesLeft, reason, headers, { retry, signal }) {
|
|
685
|
+
const delay = retryDelayMs(attempt, headers, retry);
|
|
686
|
+
const nth = attempt + 1;
|
|
687
|
+
const total = attempt + retriesLeft;
|
|
688
|
+
this.logger.info(`${tag} retrying in ${delay}ms (retry ${nth}/${total}) after ${reason}`);
|
|
689
|
+
try {
|
|
690
|
+
await sleep(delay, signal);
|
|
691
|
+
} catch (err) {
|
|
692
|
+
this.logger.info(`${tag} aborted by caller while waiting to retry`);
|
|
693
|
+
throw new APIUserAbortError(void 0, { cause: err });
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
};
|
|
697
|
+
const parseBody = async (res) => {
|
|
698
|
+
const text = await res.text();
|
|
699
|
+
if (text.length === 0) return void 0;
|
|
700
|
+
if ((res.headers.get("content-type") ?? "").includes("application/json")) try {
|
|
701
|
+
return JSON.parse(text);
|
|
702
|
+
} catch {
|
|
703
|
+
return text;
|
|
704
|
+
}
|
|
705
|
+
try {
|
|
706
|
+
return JSON.parse(text);
|
|
707
|
+
} catch {
|
|
708
|
+
return text;
|
|
709
|
+
}
|
|
710
|
+
};
|
|
711
|
+
//#endregion
|
|
712
|
+
export { APIConnectionError, APIError, APIPromise, APITimeoutError, APIUserAbortError, AuthenticationError, BadRequestError, ENV, InternalServerError, LOG_LEVELS, NotFoundError, PermissionDeniedError, RateLimitError, TypeSafeClient, TypeSafeError, UnprocessableEntityError, VERSION, choice, noul, score };
|
|
713
|
+
|
|
714
|
+
//# sourceMappingURL=index.mjs.map
|