@itsoltech/basal-client 0.4.0 → 0.5.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.
@@ -0,0 +1,206 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Batches = exports.Files = void 0;
4
+ const errors_cjs_1 = require("./errors.cjs");
5
+ const batches_cjs_1 = require("./batches.cjs");
6
+ const TERMINAL = new Set(batches_cjs_1.TERMINAL_BATCH_STATUSES);
7
+ const COMMON_KEYS = new Set(["endpoint", "completion_window", "metadata", "output_expires_after"]);
8
+ const MAX_TIMER_MS = 2_147_483_647;
9
+ function duration(value, fallback, name) {
10
+ if (value === undefined)
11
+ return fallback;
12
+ if (!Number.isFinite(value) || value <= 0 || value > MAX_TIMER_MS)
13
+ throw new TypeError(`${name} must be positive and at most ${MAX_TIMER_MS}`);
14
+ return value;
15
+ }
16
+ /** Resolves after `ms`, or rejects with `signal.reason` on abort; the timer and listener are always removed. */
17
+ function sleep(ms, signal) {
18
+ return new Promise((resolve, reject) => {
19
+ if (signal.aborted) {
20
+ reject(signal.reason);
21
+ return;
22
+ }
23
+ const onAbort = () => { clearTimeout(timer); reject(signal.reason); };
24
+ const timer = setTimeout(() => { signal.removeEventListener("abort", onAbort); resolve(); }, ms);
25
+ signal.addEventListener("abort", onAbort, { once: true });
26
+ });
27
+ }
28
+ function expiresAfter(value) {
29
+ if (value === undefined || value === null)
30
+ return;
31
+ if (typeof value !== "object" || value.anchor !== "created_at" || !Number.isSafeInteger(value.seconds) || value.seconds < 0) {
32
+ throw new TypeError("output_expires_after needs anchor created_at and whole seconds");
33
+ }
34
+ }
35
+ /** Common parameters of the `requests` form, checked before the upload so a local mistake leaves no orphan file.
36
+ * Metadata limits stay with the server, as in `createBatch`.
37
+ */
38
+ function commonParams(params) {
39
+ for (const [key, value] of Object.entries(params)) {
40
+ if (value !== undefined && key !== "requests" && !COMMON_KEYS.has(key))
41
+ throw new TypeError(`unsupported batch parameter ${JSON.stringify(key)}`);
42
+ }
43
+ const { endpoint, completion_window, metadata, output_expires_after } = params;
44
+ if (endpoint !== undefined && endpoint !== "/v1/basal")
45
+ throw new TypeError("endpoint must be /v1/basal");
46
+ if (completion_window !== undefined && completion_window !== "24h")
47
+ throw new TypeError("completion_window must be 24h");
48
+ if (metadata !== undefined && metadata !== null) {
49
+ if (typeof metadata !== "object" || Array.isArray(metadata) || Object.values(metadata).some(v => typeof v !== "string")) {
50
+ throw new TypeError("metadata must be an object of string values");
51
+ }
52
+ }
53
+ expiresAfter(output_expires_after);
54
+ const common = {};
55
+ for (const key of COMMON_KEYS)
56
+ if (params[key] !== undefined)
57
+ Object.assign(common, { [key]: params[key] });
58
+ return common;
59
+ }
60
+ function randomKey() {
61
+ const random = new Uint8Array(16);
62
+ globalThis.crypto.getRandomValues(random);
63
+ return `basal-sdk-${Array.from(random, b => b.toString(16).padStart(2, "0")).join("")}`;
64
+ }
65
+ /** `client.files`: resource names for the flat Files methods, with the same behavior. */
66
+ class Files {
67
+ #client;
68
+ constructor(client) { this.#client = client; }
69
+ /** `uploadFile`. */
70
+ create(request, options = {}) { return this.#client.uploadFile(request, options); }
71
+ /** `getFile`. */
72
+ retrieve(fileId, options = {}) { return this.#client.getFile(fileId, options); }
73
+ /** `listFiles`. */
74
+ list(options = {}) { return this.#client.listFiles(options); }
75
+ /** `deleteFile`. */
76
+ delete(fileId, options = {}) { return this.#client.deleteFile(fileId, options); }
77
+ /** `downloadFile`: raw bytes, buffered up to `maxBytes` (default `maxResponseBytes`). */
78
+ content(fileId, options = {}) { return this.#client.downloadFile(fileId, options); }
79
+ }
80
+ exports.Files = Files;
81
+ /** `client.batches`: resource names for the flat Batch methods plus `create({requests})`, `wait` and `results`.
82
+ * Nothing runs in the background and nothing is retried.
83
+ */
84
+ class Batches {
85
+ #client;
86
+ constructor(client) { this.#client = client; }
87
+ /** `{input_file_id, ...}` is `createBatch`. `{requests, ...}` validates and writes the JSONL in memory, uploads it,
88
+ * then creates the batch with `idempotencyKey` (generated when omitted). A create failure after the upload raises
89
+ * `BasalBatchSubmissionError` with the file id and key; the file is kept.
90
+ */
91
+ async create(params, options = {}) {
92
+ if (!params || typeof params !== "object")
93
+ throw new TypeError("batch parameters must be an object");
94
+ const fromFile = params.input_file_id !== undefined;
95
+ if (fromFile === (params.requests !== undefined))
96
+ throw new TypeError("pass exactly one of input_file_id or requests");
97
+ const { uploadTimeoutMs, ...createOptions } = options;
98
+ if (fromFile) {
99
+ if (uploadTimeoutMs !== undefined)
100
+ throw new TypeError("uploadTimeoutMs applies only to requests");
101
+ return this.#client.createBatch(params, createOptions);
102
+ }
103
+ const common = commonParams(params);
104
+ const idempotencyKey = createOptions.idempotencyKey ?? randomKey();
105
+ (0, batches_cjs_1.idempotencyHeader)(idempotencyKey);
106
+ duration(uploadTimeoutMs, 1, "uploadTimeoutMs");
107
+ const content = (0, batches_cjs_1.serializeBatchRequests)(params.requests);
108
+ const transfer = {};
109
+ if (createOptions.signal !== undefined)
110
+ transfer.signal = createOptions.signal;
111
+ if (uploadTimeoutMs !== undefined)
112
+ transfer.timeoutMs = uploadTimeoutMs;
113
+ const file = await this.#client.uploadFile({ file: content, filename: "batch_input.jsonl" }, transfer);
114
+ if (!file || typeof file.id !== "string" || !file.id)
115
+ throw new errors_cjs_1.BasalProtocolError("Basal upload response has no file id");
116
+ try {
117
+ return await this.#client.createBatch({ ...common, input_file_id: file.id }, { ...createOptions, idempotencyKey });
118
+ }
119
+ catch (cause) {
120
+ throw new errors_cjs_1.BasalBatchSubmissionError(file, idempotencyKey, { cause });
121
+ }
122
+ }
123
+ /** `getBatch`. */
124
+ retrieve(batchId, options = {}) { return this.#client.getBatch(batchId, options); }
125
+ /** `listBatches`. */
126
+ list(options = {}) { return this.#client.listBatches(options); }
127
+ /** `cancelBatch`: requests cancellation and returns without waiting. */
128
+ cancel(batchId, options = {}) { return this.#client.cancelBatch(batchId, options); }
129
+ /** Retrieve until the status is `completed`, `failed`, `expired` or `cancelled` and return that batch (check
130
+ * `status`, `errors` and `request_counts`). The first retrieve is immediate. `timeoutMs` bounds the whole wait,
131
+ * aborting an in-flight retrieve, and raises `BasalBatchTimeoutError` without cancelling the batch. HTTP and
132
+ * transport errors propagate without retries; an abort rejects with `BasalTransportError` (cause: the reason).
133
+ */
134
+ async wait(batchId, options = {}) {
135
+ (0, batches_cjs_1.resourcePath)("v1/batches", batchId);
136
+ const timeoutMs = duration(options.timeoutMs, 600_000, "timeoutMs");
137
+ const pollIntervalMs = duration(options.pollIntervalMs, 5_000, "pollIntervalMs");
138
+ const { signal } = options;
139
+ const controller = new AbortController();
140
+ const forward = () => controller.abort(signal?.reason);
141
+ signal?.addEventListener("abort", forward, { once: true });
142
+ if (signal?.aborted)
143
+ forward();
144
+ const expired = new Error("Basal batch wait timed out");
145
+ // Timers use a monotonic clock, so wall-clock changes do not move the deadline.
146
+ const timer = setTimeout(() => controller.abort(expired), timeoutMs);
147
+ let last = null;
148
+ try {
149
+ for (;;) {
150
+ try {
151
+ last = await this.#client.getBatch(batchId, { signal: controller.signal });
152
+ if (TERMINAL.has(last.status))
153
+ return last;
154
+ await sleep(pollIntervalMs, controller.signal);
155
+ }
156
+ catch (cause) {
157
+ if (controller.signal.reason === expired && (cause === expired || cause instanceof errors_cjs_1.BasalTransportError)) {
158
+ throw new errors_cjs_1.BasalBatchTimeoutError(batchId, last, timeoutMs, { cause });
159
+ }
160
+ if (controller.signal.aborted && cause === controller.signal.reason) {
161
+ throw new errors_cjs_1.BasalTransportError("Basal batch wait aborted", { cause });
162
+ }
163
+ throw cause;
164
+ }
165
+ }
166
+ }
167
+ finally {
168
+ clearTimeout(timer);
169
+ signal?.removeEventListener("abort", forward);
170
+ }
171
+ }
172
+ /** Records of a finished batch: the output file, then the error file (once if both ids are equal). Does not wait:
173
+ * a non-terminal batch raises `BasalBatchNotReadyError`. Expired and cancelled batches yield their partial results;
174
+ * a completed batch can yield failed records. Batch-level validation errors are in `retrieve`/`wait`, not here.
175
+ * Each file is downloaded whole (bounded by `maxBytes`) and parsed one line at a time; a malformed line raises
176
+ * `BasalBatchResultError` after the earlier records were yielded.
177
+ */
178
+ results(batchId, options = {}) {
179
+ (0, batches_cjs_1.resourcePath)("v1/batches", batchId);
180
+ duration(options.timeoutMs, 1, "timeoutMs");
181
+ if (options.maxBytes !== undefined && (!Number.isSafeInteger(options.maxBytes) || options.maxBytes <= 0)) {
182
+ throw new TypeError("maxBytes must be a positive safe integer");
183
+ }
184
+ return this.#results(batchId, options);
185
+ }
186
+ async *#results(batchId, options) {
187
+ const request = options.signal === undefined ? {} : { signal: options.signal };
188
+ const batch = await this.#client.getBatch(batchId, request);
189
+ if (!TERMINAL.has(batch.status))
190
+ throw new errors_cjs_1.BasalBatchNotReadyError(batchId, batch);
191
+ const ids = [];
192
+ for (const id of [batch.output_file_id, batch.error_file_id]) {
193
+ if (id === null || id === undefined)
194
+ continue;
195
+ if (typeof id !== "string" || !id)
196
+ throw new errors_cjs_1.BasalProtocolError("Basal batch has an invalid result file id");
197
+ if (!ids.includes(id))
198
+ ids.push(id);
199
+ }
200
+ for (const fileId of ids) {
201
+ const content = await this.#client.downloadFile(fileId, options);
202
+ yield* (0, batches_cjs_1.readBatchResultLines)(content, fileId);
203
+ }
204
+ }
205
+ }
206
+ exports.Batches = Batches;
@@ -0,0 +1,48 @@
1
+ import { type Batch, type BatchCreateOptions, type BatchCreateParams, type BatchList, type BatchResultLine, type BatchResultsOptions, type BatchWaitOptions, type DeletedFile, type DownloadOptions, type FileList, type FileObject, type ListFilesOptions, type ListOptions, type RequestOptions, type TransferOptions, type UploadFileRequest } from "./batches.cjs";
2
+ import type { BasalClient } from "./index.cjs";
3
+ /** `client.files`: resource names for the flat Files methods, with the same behavior. */
4
+ export declare class Files {
5
+ #private;
6
+ constructor(client: BasalClient);
7
+ /** `uploadFile`. */
8
+ create(request: UploadFileRequest, options?: TransferOptions): Promise<FileObject>;
9
+ /** `getFile`. */
10
+ retrieve(fileId: string, options?: RequestOptions): Promise<FileObject>;
11
+ /** `listFiles`. */
12
+ list(options?: ListFilesOptions): Promise<FileList>;
13
+ /** `deleteFile`. */
14
+ delete(fileId: string, options?: RequestOptions): Promise<DeletedFile>;
15
+ /** `downloadFile`: raw bytes, buffered up to `maxBytes` (default `maxResponseBytes`). */
16
+ content(fileId: string, options?: DownloadOptions): Promise<Uint8Array>;
17
+ }
18
+ /** `client.batches`: resource names for the flat Batch methods plus `create({requests})`, `wait` and `results`.
19
+ * Nothing runs in the background and nothing is retried.
20
+ */
21
+ export declare class Batches {
22
+ #private;
23
+ constructor(client: BasalClient);
24
+ /** `{input_file_id, ...}` is `createBatch`. `{requests, ...}` validates and writes the JSONL in memory, uploads it,
25
+ * then creates the batch with `idempotencyKey` (generated when omitted). A create failure after the upload raises
26
+ * `BasalBatchSubmissionError` with the file id and key; the file is kept.
27
+ */
28
+ create(params: BatchCreateParams, options?: BatchCreateOptions): Promise<Batch>;
29
+ /** `getBatch`. */
30
+ retrieve(batchId: string, options?: RequestOptions): Promise<Batch>;
31
+ /** `listBatches`. */
32
+ list(options?: ListOptions): Promise<BatchList>;
33
+ /** `cancelBatch`: requests cancellation and returns without waiting. */
34
+ cancel(batchId: string, options?: RequestOptions): Promise<Batch>;
35
+ /** Retrieve until the status is `completed`, `failed`, `expired` or `cancelled` and return that batch (check
36
+ * `status`, `errors` and `request_counts`). The first retrieve is immediate. `timeoutMs` bounds the whole wait,
37
+ * aborting an in-flight retrieve, and raises `BasalBatchTimeoutError` without cancelling the batch. HTTP and
38
+ * transport errors propagate without retries; an abort rejects with `BasalTransportError` (cause: the reason).
39
+ */
40
+ wait(batchId: string, options?: BatchWaitOptions): Promise<Batch>;
41
+ /** Records of a finished batch: the output file, then the error file (once if both ids are equal). Does not wait:
42
+ * a non-terminal batch raises `BasalBatchNotReadyError`. Expired and cancelled batches yield their partial results;
43
+ * a completed batch can yield failed records. Batch-level validation errors are in `retrieve`/`wait`, not here.
44
+ * Each file is downloaded whole (bounded by `maxBytes`) and parsed one line at a time; a malformed line raises
45
+ * `BasalBatchResultError` after the earlier records were yielded.
46
+ */
47
+ results(batchId: string, options?: BatchResultsOptions): AsyncGenerator<BatchResultLine, void, undefined>;
48
+ }
@@ -146,6 +146,45 @@ export interface UploadFileRequest {
146
146
  /** File lifetime (server default 30 days). */
147
147
  expires_after?: ExpiresAfter;
148
148
  }
149
+ /** Parameters shared by both `batches.create` forms. Defaults: `endpoint: "/v1/basal"`, `completion_window: "24h"`. */
150
+ export interface BatchCreateCommonParams {
151
+ endpoint?: "/v1/basal";
152
+ completion_window?: "24h";
153
+ metadata?: Metadata | null;
154
+ /** Lifetime of each result file, anchored at its creation. */
155
+ output_expires_after?: ExpiresAfter | null;
156
+ }
157
+ /** Create a batch from an uploaded input file; the same request as `createBatch`. */
158
+ export interface BatchCreateFromFileParams extends BatchCreateCommonParams {
159
+ input_file_id: string;
160
+ requests?: never;
161
+ }
162
+ /** Create a batch from records: the SDK writes JSONL in memory, uploads it (`purpose=batch`), then creates the batch. */
163
+ export interface BatchCreateFromRequestsParams extends BatchCreateCommonParams {
164
+ /** 1–50 000 records, unique `custom_id` (1–256 UTF-8 bytes), at most 200 000 000 bytes of JSONL in total. */
165
+ requests: Iterable<BatchInputLine>;
166
+ input_file_id?: never;
167
+ }
168
+ /** Exactly one of `input_file_id` or `requests`. */
169
+ export type BatchCreateParams = BatchCreateFromFileParams | BatchCreateFromRequestsParams;
170
+ export interface BatchCreateOptions extends CreateBatchOptions {
171
+ /** Deadline of the input upload (`requests` form only); defaults to the client `timeoutMs`. */
172
+ uploadTimeoutMs?: number;
173
+ }
174
+ export interface BatchWaitOptions extends RequestOptions {
175
+ /** Deadline of the whole wait, including in-flight retrieves and sleeps; default 600 000 ms. */
176
+ timeoutMs?: number;
177
+ /** Delay between retrieves; default 5000 ms. The first retrieve is immediate. */
178
+ pollIntervalMs?: number;
179
+ }
180
+ /** Each result file is downloaded whole into memory (up to `maxBytes`, default the client `maxResponseBytes`) and
181
+ * parsed one line at a time; `timeoutMs` is the deadline of each download.
182
+ */
183
+ export type BatchResultsOptions = DownloadOptions;
184
+ /** Client-side bounds of `batches.create({requests})`, equal to the server defaults; a server may be configured lower. */
185
+ export declare const MAX_BATCH_REQUESTS = 50000;
186
+ export declare const MAX_BATCH_INPUT_BYTES = 200000000;
187
+ export declare const TERMINAL_BATCH_STATUSES: readonly BatchStatus[];
149
188
  /** Path segment of a resource id. `.` and `..` would be resolved as dot segments by URL parsing. */
150
189
  export declare function resourcePath(prefix: string, id: string, suffix?: string): string;
151
190
  export declare function listPath(path: string, options: ListFilesOptions): string;
@@ -164,3 +203,12 @@ export declare function uploadForm(request: UploadFileRequest): Multipart;
164
203
  export declare function batchInput(lines: Iterable<BatchInputLine>): Uint8Array;
165
204
  /** Parse downloaded result bytes (output or error file) into lines. Never fetches anything. */
166
205
  export declare function parseBatchResults(content: Uint8Array): BatchResultLine[];
206
+ /** Validated JSONL of `batches.create({requests})`: nonempty, supported envelope, unique ids and the bounds above.
207
+ * The result is held in memory (at most `MAX_BATCH_INPUT_BYTES`). Never sends anything.
208
+ */
209
+ export declare function serializeBatchRequests(requests: Iterable<BatchInputLine>): Blob;
210
+ /** Result lines of one downloaded file, decoded and checked one line at a time (no list of all lines). A final LF is
211
+ * optional; any other empty line, invalid UTF-8 (a BOM included), JSON or envelope raises `BasalBatchResultError`.
212
+ * Unknown fields are preserved.
213
+ */
214
+ export declare function readBatchResultLines(content: Uint8Array, fileId: string): Generator<BatchResultLine, void, undefined>;
@@ -1,4 +1,8 @@
1
- import { BasalProtocolError } from "./errors.js";
1
+ import { BasalBatchResultError, BasalProtocolError } from "./errors.js";
2
+ /** Client-side bounds of `batches.create({requests})`, equal to the server defaults; a server may be configured lower. */
3
+ export const MAX_BATCH_REQUESTS = 50_000;
4
+ export const MAX_BATCH_INPUT_BYTES = 200_000_000;
5
+ export const TERMINAL_BATCH_STATUSES = Object.freeze(["completed", "failed", "expired", "cancelled"]);
2
6
  const encoder = new TextEncoder();
3
7
  /** Path segment of a resource id. `.` and `..` would be resolved as dot segments by URL parsing. */
4
8
  export function resourcePath(prefix, id, suffix = "") {
@@ -180,3 +184,117 @@ export function parseBatchResults(content) {
180
184
  return value;
181
185
  });
182
186
  }
187
+ const LINE_KEYS = new Set(["custom_id", "method", "url", "body"]);
188
+ /** Validated JSONL of `batches.create({requests})`: nonempty, supported envelope, unique ids and the bounds above.
189
+ * The result is held in memory (at most `MAX_BATCH_INPUT_BYTES`). Never sends anything.
190
+ */
191
+ export function serializeBatchRequests(requests) {
192
+ if (!requests || typeof requests !== "object" || typeof requests[Symbol.iterator] !== "function") {
193
+ throw new TypeError("requests must be an iterable of batch records");
194
+ }
195
+ const parts = [];
196
+ const seen = new Set();
197
+ let bytes = 0;
198
+ for (const line of requests) {
199
+ const at = `requests[${parts.length}]`;
200
+ if (parts.length >= MAX_BATCH_REQUESTS)
201
+ throw new TypeError(`requests must have at most ${MAX_BATCH_REQUESTS} records`);
202
+ if (!line || typeof line !== "object" || Array.isArray(line))
203
+ throw new TypeError(`${at} must be an object`);
204
+ for (const key of Object.keys(line)) {
205
+ if (!LINE_KEYS.has(key))
206
+ throw new TypeError(`${at} has unsupported field ${JSON.stringify(key)}`);
207
+ }
208
+ const { custom_id: id, body } = line;
209
+ // Lone surrogates have no UTF-8 form; the server compares ids byte for byte.
210
+ if (typeof id !== "string" || !id || /\p{Cs}/u.test(id) || encoder.encode(id).length > 256) {
211
+ throw new TypeError(`${at}.custom_id must be a string of 1-256 UTF-8 bytes`);
212
+ }
213
+ if (seen.has(id))
214
+ throw new TypeError(`${at}.custom_id duplicates an earlier record`);
215
+ seen.add(id);
216
+ if (line.method !== undefined && line.method !== "POST")
217
+ throw new TypeError(`${at}.method must be POST`);
218
+ if (line.url !== undefined && line.url !== "/v1/basal")
219
+ throw new TypeError(`${at}.url must be /v1/basal`);
220
+ if (!body || typeof body !== "object" || Array.isArray(body))
221
+ throw new TypeError(`${at}.body must be a native request object`);
222
+ if (body.stream === true)
223
+ throw new TypeError(`${at}.body.stream must not be true`);
224
+ let json;
225
+ try {
226
+ json = JSON.stringify({ custom_id: id, method: "POST", url: "/v1/basal", body });
227
+ }
228
+ catch (cause) {
229
+ throw new TypeError(`${at}.body is not serializable as JSON`, { cause });
230
+ }
231
+ const encoded = encoder.encode(json + "\n");
232
+ bytes += encoded.byteLength;
233
+ if (bytes > MAX_BATCH_INPUT_BYTES)
234
+ throw new TypeError(`requests exceed ${MAX_BATCH_INPUT_BYTES} bytes of JSONL`);
235
+ parts.push(encoded);
236
+ }
237
+ if (!parts.length)
238
+ throw new TypeError("requests must contain at least one record");
239
+ return new Blob(parts, { type: "application/jsonl" });
240
+ }
241
+ /** Result lines of one downloaded file, decoded and checked one line at a time (no list of all lines). A final LF is
242
+ * optional; any other empty line, invalid UTF-8 (a BOM included), JSON or envelope raises `BasalBatchResultError`.
243
+ * Unknown fields are preserved.
244
+ */
245
+ export function* readBatchResultLines(content, fileId) {
246
+ const decoder = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
247
+ let start = 0;
248
+ for (let number = 1; start < content.byteLength; number++) {
249
+ // UTF-8 never encodes another code point with byte 0x0A, so splitting bytes at LF is exact.
250
+ let end = content.indexOf(0x0a, start);
251
+ if (end < 0)
252
+ end = content.byteLength;
253
+ const bytes = content.subarray(start, end);
254
+ start = end + 1;
255
+ let text;
256
+ try {
257
+ text = decoder.decode(bytes);
258
+ }
259
+ catch (cause) {
260
+ throw new BasalBatchResultError(fileId, number, "is not UTF-8", { cause });
261
+ }
262
+ let value;
263
+ try {
264
+ value = JSON.parse(text);
265
+ }
266
+ catch (cause) {
267
+ throw new BasalBatchResultError(fileId, number, "is not JSON", { cause });
268
+ }
269
+ const problem = envelopeProblem(value);
270
+ if (problem)
271
+ throw new BasalBatchResultError(fileId, number, problem);
272
+ yield value;
273
+ }
274
+ }
275
+ function isObject(value) {
276
+ return value !== null && typeof value === "object" && !Array.isArray(value);
277
+ }
278
+ function envelopeProblem(value) {
279
+ if (!isObject(value))
280
+ return "is not a JSON object";
281
+ if (typeof value.id !== "string" || !value.id)
282
+ return "has no string id";
283
+ if (typeof value.custom_id !== "string" || !value.custom_id)
284
+ return "has no string custom_id";
285
+ const { response, error } = value;
286
+ if (response === undefined || error === undefined)
287
+ return "lacks response or error";
288
+ if (response !== null && (!isObject(response) || !Number.isSafeInteger(response.status_code)
289
+ || response.status_code < 100 || response.status_code > 599
290
+ || typeof response.request_id !== "string" || !response.request_id || !isObject(response.body)))
291
+ return "has an invalid response";
292
+ if (error !== null && (!isObject(error) || typeof error.code !== "string" || !error.code || typeof error.message !== "string")) {
293
+ return "has an invalid error";
294
+ }
295
+ if (response === null && error === null)
296
+ return "has neither response nor error";
297
+ if (response !== null && error !== null)
298
+ return "has both response and error";
299
+ return undefined;
300
+ }
@@ -1,3 +1,4 @@
1
+ import type { Batch, FileObject } from "./batches.js";
1
2
  export declare class BasalError extends Error {
2
3
  }
3
4
  /** Non-2xx response. Server body and Retry-After are available without logging them. */
@@ -13,3 +14,46 @@ export declare class BasalProtocolError extends BasalError {
13
14
  }
14
15
  export declare class BasalResponseTooLargeError extends BasalError {
15
16
  }
17
+ /** `batches.create({requests})` uploaded the input file, but creating the batch failed (HTTP, transport or abort).
18
+ * The SDK neither retries nor deletes the file. The create may still have succeeded on the server, so resume with
19
+ * the same parameters, `{input_file_id: error.fileId}` and `{idempotencyKey: error.idempotencyKey}`; the original
20
+ * failure is in `cause`.
21
+ */
22
+ export declare class BasalBatchSubmissionError extends BasalError {
23
+ readonly file: FileObject;
24
+ readonly idempotencyKey: string;
25
+ readonly fileId: string;
26
+ constructor(file: FileObject, idempotencyKey: string, options: {
27
+ cause: unknown;
28
+ });
29
+ }
30
+ /** `batches.wait` reached its deadline. The batch keeps running on the server; nothing is cancelled.
31
+ * `lastBatch` is the last retrieved state (null if no retrieve finished) and is non-enumerable, like `BasalHTTPError.body`.
32
+ */
33
+ export declare class BasalBatchTimeoutError extends BasalError {
34
+ readonly batchId: string;
35
+ readonly lastBatch: Batch | null;
36
+ readonly timeoutMs: number;
37
+ constructor(batchId: string, lastBatch: Batch | null, timeoutMs: number, options?: {
38
+ cause: unknown;
39
+ });
40
+ }
41
+ /** `batches.results` was called for a batch that is not `completed`, `failed`, `expired` or `cancelled`.
42
+ * `batch` (non-enumerable) is the retrieved state; `status` is its raw status.
43
+ */
44
+ export declare class BasalBatchNotReadyError extends BasalError {
45
+ readonly batchId: string;
46
+ readonly batch: Batch;
47
+ readonly status: string;
48
+ constructor(batchId: string, batch: Batch);
49
+ }
50
+ /** A result file line that is not UTF-8, not JSON or not a result envelope. `line` is 1-based; the content is not
51
+ * copied into the error.
52
+ */
53
+ export declare class BasalBatchResultError extends BasalProtocolError {
54
+ readonly fileId: string;
55
+ readonly line: number;
56
+ constructor(fileId: string, line: number, reason: string, options?: {
57
+ cause: unknown;
58
+ });
59
+ }
@@ -21,3 +21,65 @@ export class BasalProtocolError extends BasalError {
21
21
  }
22
22
  export class BasalResponseTooLargeError extends BasalError {
23
23
  }
24
+ /** `batches.create({requests})` uploaded the input file, but creating the batch failed (HTTP, transport or abort).
25
+ * The SDK neither retries nor deletes the file. The create may still have succeeded on the server, so resume with
26
+ * the same parameters, `{input_file_id: error.fileId}` and `{idempotencyKey: error.idempotencyKey}`; the original
27
+ * failure is in `cause`.
28
+ */
29
+ export class BasalBatchSubmissionError extends BasalError {
30
+ file;
31
+ idempotencyKey;
32
+ fileId;
33
+ constructor(file, idempotencyKey, options) {
34
+ super(`Basal batch creation failed after uploading input file ${file.id}`, options);
35
+ this.file = file;
36
+ this.idempotencyKey = idempotencyKey;
37
+ this.name = "BasalBatchSubmissionError";
38
+ this.fileId = file.id;
39
+ }
40
+ }
41
+ /** `batches.wait` reached its deadline. The batch keeps running on the server; nothing is cancelled.
42
+ * `lastBatch` is the last retrieved state (null if no retrieve finished) and is non-enumerable, like `BasalHTTPError.body`.
43
+ */
44
+ export class BasalBatchTimeoutError extends BasalError {
45
+ batchId;
46
+ lastBatch;
47
+ timeoutMs;
48
+ constructor(batchId, lastBatch, timeoutMs, options) {
49
+ super(`Basal batch did not reach a terminal status within ${timeoutMs} ms`, options);
50
+ this.batchId = batchId;
51
+ this.lastBatch = lastBatch;
52
+ this.timeoutMs = timeoutMs;
53
+ this.name = "BasalBatchTimeoutError";
54
+ Object.defineProperty(this, "lastBatch", { enumerable: false });
55
+ }
56
+ }
57
+ /** `batches.results` was called for a batch that is not `completed`, `failed`, `expired` or `cancelled`.
58
+ * `batch` (non-enumerable) is the retrieved state; `status` is its raw status.
59
+ */
60
+ export class BasalBatchNotReadyError extends BasalError {
61
+ batchId;
62
+ batch;
63
+ status;
64
+ constructor(batchId, batch) {
65
+ super("Basal batch has not reached a terminal status");
66
+ this.batchId = batchId;
67
+ this.batch = batch;
68
+ this.name = "BasalBatchNotReadyError";
69
+ this.status = String(batch.status);
70
+ Object.defineProperty(this, "batch", { enumerable: false });
71
+ }
72
+ }
73
+ /** A result file line that is not UTF-8, not JSON or not a result envelope. `line` is 1-based; the content is not
74
+ * copied into the error.
75
+ */
76
+ export class BasalBatchResultError extends BasalProtocolError {
77
+ fileId;
78
+ line;
79
+ constructor(fileId, line, reason, options) {
80
+ super(`Batch result file ${fileId} line ${line} ${reason}`, options);
81
+ this.fileId = fileId;
82
+ this.line = line;
83
+ this.name = "BasalBatchResultError";
84
+ }
85
+ }
@@ -1,11 +1,13 @@
1
1
  import { type QuestionSet, type TypedDecisionResponse, type DecisionOptions } from "./questions.js";
2
2
  import { type Batch, type BatchList, type CreateBatchOptions, type CreateBatchRequest, type DeletedFile, type DownloadOptions, type FileList, type FileObject, type ListFilesOptions, type ListOptions, type RequestOptions, type TransferOptions, type UploadFileRequest } from "./batches.js";
3
+ import { Batches, Files } from "./resources.js";
3
4
  export * from "./questions.js";
4
5
  export * from "./media.js";
5
6
  export * from "./tasks.js";
6
7
  export * from "./errors.js";
7
- export { batchInput, parseBatchResults } from "./batches.js";
8
- export type { Batch, BatchErrorCode, BatchErrorEntry, BatchErrors, BatchInputLine, BatchList, BatchResultLine, BatchStatus, CreateBatchOptions, CreateBatchRequest, DeletedFile, DownloadOptions, ExpiresAfter, FileList, FileObject, FilePurpose, ListFilesOptions, ListOptions, ListPage, Metadata, RequestCounts, RequestOptions, TransferOptions, UploadContent, UploadFileRequest, } from "./batches.js";
8
+ export { batchInput, MAX_BATCH_INPUT_BYTES, MAX_BATCH_REQUESTS, parseBatchResults, TERMINAL_BATCH_STATUSES, } from "./batches.js";
9
+ export { Batches, Files } from "./resources.js";
10
+ export type { Batch, BatchCreateCommonParams, BatchCreateFromFileParams, BatchCreateFromRequestsParams, BatchCreateOptions, BatchCreateParams, BatchErrorCode, BatchErrorEntry, BatchErrors, BatchInputLine, BatchList, BatchResultLine, BatchResultsOptions, BatchStatus, BatchWaitOptions, CreateBatchOptions, CreateBatchRequest, DeletedFile, DownloadOptions, ExpiresAfter, FileList, FileObject, FilePurpose, ListFilesOptions, ListOptions, ListPage, Metadata, RequestCounts, RequestOptions, TransferOptions, UploadContent, UploadFileRequest, } from "./batches.js";
9
11
  import type { TaskAnswer } from "./tasks.js";
10
12
  /** Native Basal wire types. Criterion order is significant; do not sort keys. */
11
13
  export type JsonValue = string | number | boolean | null | JsonValue[] | {
@@ -189,6 +191,10 @@ export declare class BasalClient {
189
191
  private readonly timeoutMs;
190
192
  private readonly maxResponseBytes;
191
193
  private readonly fetch;
194
+ /** Files API under resource names: `create`, `retrieve`, `list`, `delete`, `content`. */
195
+ readonly files: Files;
196
+ /** Batch API under resource names, with `create({requests})`, `wait` and `results`. */
197
+ readonly batches: Batches;
192
198
  constructor(options?: BasalClientOptions);
193
199
  private request;
194
200
  private call;
package/dist/esm/index.js CHANGED
@@ -1,11 +1,13 @@
1
1
  import { buildRequest } from "./questions.js";
2
2
  import { BasalError, BasalHTTPError, BasalProtocolError, BasalResponseTooLargeError, BasalTransportError } from "./errors.js";
3
3
  import { createBody, idempotencyHeader, listPath, resourcePath, uploadForm, } from "./batches.js";
4
+ import { Batches, Files } from "./resources.js";
4
5
  export * from "./questions.js";
5
6
  export * from "./media.js";
6
7
  export * from "./tasks.js";
7
8
  export * from "./errors.js";
8
- export { batchInput, parseBatchResults } from "./batches.js";
9
+ export { batchInput, MAX_BATCH_INPUT_BYTES, MAX_BATCH_REQUESTS, parseBatchResults, TERMINAL_BATCH_STATUSES, } from "./batches.js";
10
+ export { Batches, Files } from "./resources.js";
9
11
  async function readBody(response, limit, signal) {
10
12
  // Count bytes from the actual stream, including decompressed responses; never trust Content-Length alone.
11
13
  const reader = response.body?.getReader();
@@ -69,6 +71,10 @@ export class BasalClient {
69
71
  timeoutMs;
70
72
  maxResponseBytes;
71
73
  fetch;
74
+ /** Files API under resource names: `create`, `retrieve`, `list`, `delete`, `content`. */
75
+ files;
76
+ /** Batch API under resource names, with `create({requests})`, `wait` and `results`. */
77
+ batches;
72
78
  constructor(options = {}) {
73
79
  const url = new URL(options.baseUrl ?? "http://127.0.0.1:8000");
74
80
  if (!["http:", "https:"].includes(url.protocol) || url.username || url.password || url.href.includes("?") || url.href.includes("#")) {
@@ -93,6 +99,8 @@ export class BasalClient {
93
99
  throw new TypeError("apiKey must be a nonempty, single-line token");
94
100
  this.headers.Authorization = `Bearer ${options.apiKey}`;
95
101
  }
102
+ this.files = new Files(this);
103
+ this.batches = new Batches(this);
96
104
  }
97
105
  request(method, path, body, signal) {
98
106
  return this.call({ method, path, json: body, signal });