@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.
- package/README.md +77 -3
- package/dist/cjs/batches.cjs +121 -0
- package/dist/cjs/batches.d.cts +48 -0
- package/dist/cjs/errors.cjs +67 -1
- package/dist/cjs/errors.d.cts +44 -0
- package/dist/cjs/index.cjs +14 -1
- package/dist/cjs/index.d.cts +8 -2
- package/dist/cjs/resources.cjs +206 -0
- package/dist/cjs/resources.d.cts +48 -0
- package/dist/esm/batches.d.ts +48 -0
- package/dist/esm/batches.js +119 -1
- package/dist/esm/errors.d.ts +44 -0
- package/dist/esm/errors.js +62 -0
- package/dist/esm/index.d.ts +8 -2
- package/dist/esm/index.js +9 -1
- package/dist/esm/resources.d.ts +48 -0
- package/dist/esm/resources.js +201 -0
- package/package.json +1 -1
|
@@ -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.js";
|
|
2
|
+
import type { BasalClient } from "./index.js";
|
|
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
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { BasalBatchNotReadyError, BasalBatchSubmissionError, BasalBatchTimeoutError, BasalProtocolError, BasalTransportError, } from "./errors.js";
|
|
2
|
+
import { idempotencyHeader, readBatchResultLines, resourcePath, serializeBatchRequests, TERMINAL_BATCH_STATUSES, } from "./batches.js";
|
|
3
|
+
const TERMINAL = new Set(TERMINAL_BATCH_STATUSES);
|
|
4
|
+
const COMMON_KEYS = new Set(["endpoint", "completion_window", "metadata", "output_expires_after"]);
|
|
5
|
+
const MAX_TIMER_MS = 2_147_483_647;
|
|
6
|
+
function duration(value, fallback, name) {
|
|
7
|
+
if (value === undefined)
|
|
8
|
+
return fallback;
|
|
9
|
+
if (!Number.isFinite(value) || value <= 0 || value > MAX_TIMER_MS)
|
|
10
|
+
throw new TypeError(`${name} must be positive and at most ${MAX_TIMER_MS}`);
|
|
11
|
+
return value;
|
|
12
|
+
}
|
|
13
|
+
/** Resolves after `ms`, or rejects with `signal.reason` on abort; the timer and listener are always removed. */
|
|
14
|
+
function sleep(ms, signal) {
|
|
15
|
+
return new Promise((resolve, reject) => {
|
|
16
|
+
if (signal.aborted) {
|
|
17
|
+
reject(signal.reason);
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
const onAbort = () => { clearTimeout(timer); reject(signal.reason); };
|
|
21
|
+
const timer = setTimeout(() => { signal.removeEventListener("abort", onAbort); resolve(); }, ms);
|
|
22
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
function expiresAfter(value) {
|
|
26
|
+
if (value === undefined || value === null)
|
|
27
|
+
return;
|
|
28
|
+
if (typeof value !== "object" || value.anchor !== "created_at" || !Number.isSafeInteger(value.seconds) || value.seconds < 0) {
|
|
29
|
+
throw new TypeError("output_expires_after needs anchor created_at and whole seconds");
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/** Common parameters of the `requests` form, checked before the upload so a local mistake leaves no orphan file.
|
|
33
|
+
* Metadata limits stay with the server, as in `createBatch`.
|
|
34
|
+
*/
|
|
35
|
+
function commonParams(params) {
|
|
36
|
+
for (const [key, value] of Object.entries(params)) {
|
|
37
|
+
if (value !== undefined && key !== "requests" && !COMMON_KEYS.has(key))
|
|
38
|
+
throw new TypeError(`unsupported batch parameter ${JSON.stringify(key)}`);
|
|
39
|
+
}
|
|
40
|
+
const { endpoint, completion_window, metadata, output_expires_after } = params;
|
|
41
|
+
if (endpoint !== undefined && endpoint !== "/v1/basal")
|
|
42
|
+
throw new TypeError("endpoint must be /v1/basal");
|
|
43
|
+
if (completion_window !== undefined && completion_window !== "24h")
|
|
44
|
+
throw new TypeError("completion_window must be 24h");
|
|
45
|
+
if (metadata !== undefined && metadata !== null) {
|
|
46
|
+
if (typeof metadata !== "object" || Array.isArray(metadata) || Object.values(metadata).some(v => typeof v !== "string")) {
|
|
47
|
+
throw new TypeError("metadata must be an object of string values");
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
expiresAfter(output_expires_after);
|
|
51
|
+
const common = {};
|
|
52
|
+
for (const key of COMMON_KEYS)
|
|
53
|
+
if (params[key] !== undefined)
|
|
54
|
+
Object.assign(common, { [key]: params[key] });
|
|
55
|
+
return common;
|
|
56
|
+
}
|
|
57
|
+
function randomKey() {
|
|
58
|
+
const random = new Uint8Array(16);
|
|
59
|
+
globalThis.crypto.getRandomValues(random);
|
|
60
|
+
return `basal-sdk-${Array.from(random, b => b.toString(16).padStart(2, "0")).join("")}`;
|
|
61
|
+
}
|
|
62
|
+
/** `client.files`: resource names for the flat Files methods, with the same behavior. */
|
|
63
|
+
export class Files {
|
|
64
|
+
#client;
|
|
65
|
+
constructor(client) { this.#client = client; }
|
|
66
|
+
/** `uploadFile`. */
|
|
67
|
+
create(request, options = {}) { return this.#client.uploadFile(request, options); }
|
|
68
|
+
/** `getFile`. */
|
|
69
|
+
retrieve(fileId, options = {}) { return this.#client.getFile(fileId, options); }
|
|
70
|
+
/** `listFiles`. */
|
|
71
|
+
list(options = {}) { return this.#client.listFiles(options); }
|
|
72
|
+
/** `deleteFile`. */
|
|
73
|
+
delete(fileId, options = {}) { return this.#client.deleteFile(fileId, options); }
|
|
74
|
+
/** `downloadFile`: raw bytes, buffered up to `maxBytes` (default `maxResponseBytes`). */
|
|
75
|
+
content(fileId, options = {}) { return this.#client.downloadFile(fileId, options); }
|
|
76
|
+
}
|
|
77
|
+
/** `client.batches`: resource names for the flat Batch methods plus `create({requests})`, `wait` and `results`.
|
|
78
|
+
* Nothing runs in the background and nothing is retried.
|
|
79
|
+
*/
|
|
80
|
+
export class Batches {
|
|
81
|
+
#client;
|
|
82
|
+
constructor(client) { this.#client = client; }
|
|
83
|
+
/** `{input_file_id, ...}` is `createBatch`. `{requests, ...}` validates and writes the JSONL in memory, uploads it,
|
|
84
|
+
* then creates the batch with `idempotencyKey` (generated when omitted). A create failure after the upload raises
|
|
85
|
+
* `BasalBatchSubmissionError` with the file id and key; the file is kept.
|
|
86
|
+
*/
|
|
87
|
+
async create(params, options = {}) {
|
|
88
|
+
if (!params || typeof params !== "object")
|
|
89
|
+
throw new TypeError("batch parameters must be an object");
|
|
90
|
+
const fromFile = params.input_file_id !== undefined;
|
|
91
|
+
if (fromFile === (params.requests !== undefined))
|
|
92
|
+
throw new TypeError("pass exactly one of input_file_id or requests");
|
|
93
|
+
const { uploadTimeoutMs, ...createOptions } = options;
|
|
94
|
+
if (fromFile) {
|
|
95
|
+
if (uploadTimeoutMs !== undefined)
|
|
96
|
+
throw new TypeError("uploadTimeoutMs applies only to requests");
|
|
97
|
+
return this.#client.createBatch(params, createOptions);
|
|
98
|
+
}
|
|
99
|
+
const common = commonParams(params);
|
|
100
|
+
const idempotencyKey = createOptions.idempotencyKey ?? randomKey();
|
|
101
|
+
idempotencyHeader(idempotencyKey);
|
|
102
|
+
duration(uploadTimeoutMs, 1, "uploadTimeoutMs");
|
|
103
|
+
const content = serializeBatchRequests(params.requests);
|
|
104
|
+
const transfer = {};
|
|
105
|
+
if (createOptions.signal !== undefined)
|
|
106
|
+
transfer.signal = createOptions.signal;
|
|
107
|
+
if (uploadTimeoutMs !== undefined)
|
|
108
|
+
transfer.timeoutMs = uploadTimeoutMs;
|
|
109
|
+
const file = await this.#client.uploadFile({ file: content, filename: "batch_input.jsonl" }, transfer);
|
|
110
|
+
if (!file || typeof file.id !== "string" || !file.id)
|
|
111
|
+
throw new BasalProtocolError("Basal upload response has no file id");
|
|
112
|
+
try {
|
|
113
|
+
return await this.#client.createBatch({ ...common, input_file_id: file.id }, { ...createOptions, idempotencyKey });
|
|
114
|
+
}
|
|
115
|
+
catch (cause) {
|
|
116
|
+
throw new BasalBatchSubmissionError(file, idempotencyKey, { cause });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/** `getBatch`. */
|
|
120
|
+
retrieve(batchId, options = {}) { return this.#client.getBatch(batchId, options); }
|
|
121
|
+
/** `listBatches`. */
|
|
122
|
+
list(options = {}) { return this.#client.listBatches(options); }
|
|
123
|
+
/** `cancelBatch`: requests cancellation and returns without waiting. */
|
|
124
|
+
cancel(batchId, options = {}) { return this.#client.cancelBatch(batchId, options); }
|
|
125
|
+
/** Retrieve until the status is `completed`, `failed`, `expired` or `cancelled` and return that batch (check
|
|
126
|
+
* `status`, `errors` and `request_counts`). The first retrieve is immediate. `timeoutMs` bounds the whole wait,
|
|
127
|
+
* aborting an in-flight retrieve, and raises `BasalBatchTimeoutError` without cancelling the batch. HTTP and
|
|
128
|
+
* transport errors propagate without retries; an abort rejects with `BasalTransportError` (cause: the reason).
|
|
129
|
+
*/
|
|
130
|
+
async wait(batchId, options = {}) {
|
|
131
|
+
resourcePath("v1/batches", batchId);
|
|
132
|
+
const timeoutMs = duration(options.timeoutMs, 600_000, "timeoutMs");
|
|
133
|
+
const pollIntervalMs = duration(options.pollIntervalMs, 5_000, "pollIntervalMs");
|
|
134
|
+
const { signal } = options;
|
|
135
|
+
const controller = new AbortController();
|
|
136
|
+
const forward = () => controller.abort(signal?.reason);
|
|
137
|
+
signal?.addEventListener("abort", forward, { once: true });
|
|
138
|
+
if (signal?.aborted)
|
|
139
|
+
forward();
|
|
140
|
+
const expired = new Error("Basal batch wait timed out");
|
|
141
|
+
// Timers use a monotonic clock, so wall-clock changes do not move the deadline.
|
|
142
|
+
const timer = setTimeout(() => controller.abort(expired), timeoutMs);
|
|
143
|
+
let last = null;
|
|
144
|
+
try {
|
|
145
|
+
for (;;) {
|
|
146
|
+
try {
|
|
147
|
+
last = await this.#client.getBatch(batchId, { signal: controller.signal });
|
|
148
|
+
if (TERMINAL.has(last.status))
|
|
149
|
+
return last;
|
|
150
|
+
await sleep(pollIntervalMs, controller.signal);
|
|
151
|
+
}
|
|
152
|
+
catch (cause) {
|
|
153
|
+
if (controller.signal.reason === expired && (cause === expired || cause instanceof BasalTransportError)) {
|
|
154
|
+
throw new BasalBatchTimeoutError(batchId, last, timeoutMs, { cause });
|
|
155
|
+
}
|
|
156
|
+
if (controller.signal.aborted && cause === controller.signal.reason) {
|
|
157
|
+
throw new BasalTransportError("Basal batch wait aborted", { cause });
|
|
158
|
+
}
|
|
159
|
+
throw cause;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
finally {
|
|
164
|
+
clearTimeout(timer);
|
|
165
|
+
signal?.removeEventListener("abort", forward);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
/** Records of a finished batch: the output file, then the error file (once if both ids are equal). Does not wait:
|
|
169
|
+
* a non-terminal batch raises `BasalBatchNotReadyError`. Expired and cancelled batches yield their partial results;
|
|
170
|
+
* a completed batch can yield failed records. Batch-level validation errors are in `retrieve`/`wait`, not here.
|
|
171
|
+
* Each file is downloaded whole (bounded by `maxBytes`) and parsed one line at a time; a malformed line raises
|
|
172
|
+
* `BasalBatchResultError` after the earlier records were yielded.
|
|
173
|
+
*/
|
|
174
|
+
results(batchId, options = {}) {
|
|
175
|
+
resourcePath("v1/batches", batchId);
|
|
176
|
+
duration(options.timeoutMs, 1, "timeoutMs");
|
|
177
|
+
if (options.maxBytes !== undefined && (!Number.isSafeInteger(options.maxBytes) || options.maxBytes <= 0)) {
|
|
178
|
+
throw new TypeError("maxBytes must be a positive safe integer");
|
|
179
|
+
}
|
|
180
|
+
return this.#results(batchId, options);
|
|
181
|
+
}
|
|
182
|
+
async *#results(batchId, options) {
|
|
183
|
+
const request = options.signal === undefined ? {} : { signal: options.signal };
|
|
184
|
+
const batch = await this.#client.getBatch(batchId, request);
|
|
185
|
+
if (!TERMINAL.has(batch.status))
|
|
186
|
+
throw new BasalBatchNotReadyError(batchId, batch);
|
|
187
|
+
const ids = [];
|
|
188
|
+
for (const id of [batch.output_file_id, batch.error_file_id]) {
|
|
189
|
+
if (id === null || id === undefined)
|
|
190
|
+
continue;
|
|
191
|
+
if (typeof id !== "string" || !id)
|
|
192
|
+
throw new BasalProtocolError("Basal batch has an invalid result file id");
|
|
193
|
+
if (!ids.includes(id))
|
|
194
|
+
ids.push(id);
|
|
195
|
+
}
|
|
196
|
+
for (const fileId of ids) {
|
|
197
|
+
const content = await this.#client.downloadFile(fileId, options);
|
|
198
|
+
yield* readBatchResultLines(content, fileId);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|