@itsoltech/basal-client 0.3.0 → 0.4.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 +55 -1
- package/dist/cjs/batches.cjs +191 -0
- package/dist/cjs/batches.d.cts +166 -0
- package/dist/cjs/errors.cjs +31 -0
- package/dist/cjs/errors.d.cts +15 -0
- package/dist/cjs/index.cjs +118 -55
- package/dist/cjs/index.d.cts +17 -15
- package/dist/esm/batches.d.ts +166 -0
- package/dist/esm/batches.js +182 -0
- package/dist/esm/errors.d.ts +15 -0
- package/dist/esm/errors.js +23 -0
- package/dist/esm/index.d.ts +17 -15
- package/dist/esm/index.js +106 -40
- package/package.json +1 -1
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export declare class BasalError extends Error {
|
|
2
|
+
}
|
|
3
|
+
/** Non-2xx response. Server body and Retry-After are available without logging them. */
|
|
4
|
+
export declare class BasalHTTPError extends BasalError {
|
|
5
|
+
readonly statusCode: number;
|
|
6
|
+
readonly body: unknown;
|
|
7
|
+
readonly retryAfter: string | null;
|
|
8
|
+
constructor(statusCode: number, body: unknown, retryAfter: string | null);
|
|
9
|
+
}
|
|
10
|
+
export declare class BasalTransportError extends BasalError {
|
|
11
|
+
}
|
|
12
|
+
export declare class BasalProtocolError extends BasalError {
|
|
13
|
+
}
|
|
14
|
+
export declare class BasalResponseTooLargeError extends BasalError {
|
|
15
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export class BasalError extends Error {
|
|
2
|
+
}
|
|
3
|
+
/** Non-2xx response. Server body and Retry-After are available without logging them. */
|
|
4
|
+
export class BasalHTTPError extends BasalError {
|
|
5
|
+
statusCode;
|
|
6
|
+
body;
|
|
7
|
+
retryAfter;
|
|
8
|
+
constructor(statusCode, body, retryAfter) {
|
|
9
|
+
super(`Basal HTTP ${statusCode}`);
|
|
10
|
+
this.statusCode = statusCode;
|
|
11
|
+
this.body = body;
|
|
12
|
+
this.retryAfter = retryAfter;
|
|
13
|
+
this.name = "BasalHTTPError";
|
|
14
|
+
// Node's default inspection and JSON.stringify must not print reflected state or credentials.
|
|
15
|
+
Object.defineProperty(this, "body", { enumerable: false });
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export class BasalTransportError extends BasalError {
|
|
19
|
+
}
|
|
20
|
+
export class BasalProtocolError extends BasalError {
|
|
21
|
+
}
|
|
22
|
+
export class BasalResponseTooLargeError extends BasalError {
|
|
23
|
+
}
|
package/dist/esm/index.d.ts
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { type QuestionSet, type TypedDecisionResponse, type DecisionOptions } from "./questions.js";
|
|
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";
|
|
2
3
|
export * from "./questions.js";
|
|
3
4
|
export * from "./media.js";
|
|
4
5
|
export * from "./tasks.js";
|
|
6
|
+
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";
|
|
5
9
|
import type { TaskAnswer } from "./tasks.js";
|
|
6
10
|
/** Native Basal wire types. Criterion order is significant; do not sort keys. */
|
|
7
11
|
export type JsonValue = string | number | boolean | null | JsonValue[] | {
|
|
@@ -164,21 +168,6 @@ export interface ModelMetadata {
|
|
|
164
168
|
export interface ModelsResponse {
|
|
165
169
|
models: ModelMetadata[];
|
|
166
170
|
}
|
|
167
|
-
export declare class BasalError extends Error {
|
|
168
|
-
}
|
|
169
|
-
/** Non-2xx response. Server body and Retry-After are available without logging them. */
|
|
170
|
-
export declare class BasalHTTPError extends BasalError {
|
|
171
|
-
readonly statusCode: number;
|
|
172
|
-
readonly body: unknown;
|
|
173
|
-
readonly retryAfter: string | null;
|
|
174
|
-
constructor(statusCode: number, body: unknown, retryAfter: string | null);
|
|
175
|
-
}
|
|
176
|
-
export declare class BasalTransportError extends BasalError {
|
|
177
|
-
}
|
|
178
|
-
export declare class BasalProtocolError extends BasalError {
|
|
179
|
-
}
|
|
180
|
-
export declare class BasalResponseTooLargeError extends BasalError {
|
|
181
|
-
}
|
|
182
171
|
/** Web Fetch input accepted by browsers and server runtimes. */
|
|
183
172
|
export type RequestInput = string | URL | Request;
|
|
184
173
|
/** A transport adapter must honor signal/redirect and return a standard Web Response. */
|
|
@@ -202,7 +191,20 @@ export declare class BasalClient {
|
|
|
202
191
|
private readonly fetch;
|
|
203
192
|
constructor(options?: BasalClientOptions);
|
|
204
193
|
private request;
|
|
194
|
+
private call;
|
|
205
195
|
decide(request: DecisionRequest, signal?: AbortSignal): Promise<DecisionResponse>;
|
|
206
196
|
decide<const Q extends QuestionSet>(state: JsonValue, questions: Q, options?: DecisionOptions): Promise<TypedDecisionResponse<Q>>;
|
|
207
197
|
models(signal?: AbortSignal): Promise<ModelsResponse>;
|
|
198
|
+
/** Upload a JSONL input file (`purpose=batch`) from caller-owned bytes, a Blob or a stream; never fetches URLs. */
|
|
199
|
+
uploadFile(request: UploadFileRequest, options?: TransferOptions): Promise<FileObject>;
|
|
200
|
+
listFiles(options?: ListFilesOptions): Promise<FileList>;
|
|
201
|
+
getFile(fileId: string, options?: RequestOptions): Promise<FileObject>;
|
|
202
|
+
deleteFile(fileId: string, options?: RequestOptions): Promise<DeletedFile>;
|
|
203
|
+
/** Raw file bytes (JSONL), buffered up to `maxBytes` (default `maxResponseBytes`); never JSON-parsed. */
|
|
204
|
+
downloadFile(fileId: string, options?: DownloadOptions): Promise<Uint8Array>;
|
|
205
|
+
createBatch(request: CreateBatchRequest, options?: CreateBatchOptions): Promise<Batch>;
|
|
206
|
+
listBatches(options?: ListOptions): Promise<BatchList>;
|
|
207
|
+
getBatch(batchId: string, options?: RequestOptions): Promise<Batch>;
|
|
208
|
+
/** Request cancellation; the batch moves to `cancelling` and later `cancelled`. Does not wait. */
|
|
209
|
+
cancelBatch(batchId: string, options?: RequestOptions): Promise<Batch>;
|
|
208
210
|
}
|
package/dist/esm/index.js
CHANGED
|
@@ -1,40 +1,20 @@
|
|
|
1
1
|
import { buildRequest } from "./questions.js";
|
|
2
|
+
import { BasalError, BasalHTTPError, BasalProtocolError, BasalResponseTooLargeError, BasalTransportError } from "./errors.js";
|
|
3
|
+
import { createBody, idempotencyHeader, listPath, resourcePath, uploadForm, } from "./batches.js";
|
|
2
4
|
export * from "./questions.js";
|
|
3
5
|
export * from "./media.js";
|
|
4
6
|
export * from "./tasks.js";
|
|
5
|
-
export
|
|
6
|
-
}
|
|
7
|
-
/** Non-2xx response. Server body and Retry-After are available without logging them. */
|
|
8
|
-
export class BasalHTTPError extends BasalError {
|
|
9
|
-
statusCode;
|
|
10
|
-
body;
|
|
11
|
-
retryAfter;
|
|
12
|
-
constructor(statusCode, body, retryAfter) {
|
|
13
|
-
super(`Basal HTTP ${statusCode}`);
|
|
14
|
-
this.statusCode = statusCode;
|
|
15
|
-
this.body = body;
|
|
16
|
-
this.retryAfter = retryAfter;
|
|
17
|
-
this.name = "BasalHTTPError";
|
|
18
|
-
// Node's default inspection and JSON.stringify must not print reflected state or credentials.
|
|
19
|
-
Object.defineProperty(this, "body", { enumerable: false });
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
export class BasalTransportError extends BasalError {
|
|
23
|
-
}
|
|
24
|
-
export class BasalProtocolError extends BasalError {
|
|
25
|
-
}
|
|
26
|
-
export class BasalResponseTooLargeError extends BasalError {
|
|
27
|
-
}
|
|
7
|
+
export * from "./errors.js";
|
|
8
|
+
export { batchInput, parseBatchResults } from "./batches.js";
|
|
28
9
|
async function readBody(response, limit, signal) {
|
|
29
10
|
// Count bytes from the actual stream, including decompressed responses; never trust Content-Length alone.
|
|
30
11
|
const reader = response.body?.getReader();
|
|
31
12
|
if (!reader)
|
|
32
|
-
return
|
|
13
|
+
return new Uint8Array();
|
|
33
14
|
const cancel = () => { void reader.cancel().catch(() => { }); };
|
|
34
15
|
signal.addEventListener("abort", cancel, { once: true });
|
|
35
16
|
if (signal.aborted)
|
|
36
17
|
cancel();
|
|
37
|
-
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
38
18
|
const parts = [];
|
|
39
19
|
let bytes = 0;
|
|
40
20
|
let complete = false;
|
|
@@ -49,18 +29,36 @@ async function readBody(response, limit, signal) {
|
|
|
49
29
|
if (bytes > limit) {
|
|
50
30
|
throw new BasalResponseTooLargeError(`Basal response exceeds ${limit} bytes`);
|
|
51
31
|
}
|
|
52
|
-
parts.push(
|
|
32
|
+
parts.push(value);
|
|
53
33
|
}
|
|
54
|
-
parts.push(decoder.decode());
|
|
55
|
-
return parts.join("");
|
|
56
34
|
}
|
|
57
35
|
finally {
|
|
58
36
|
signal.removeEventListener("abort", cancel);
|
|
59
|
-
// A
|
|
37
|
+
// A read failure must also stop the underlying HTTP body, before releasing ownership.
|
|
60
38
|
if (!complete)
|
|
61
39
|
cancel();
|
|
62
40
|
reader.releaseLock();
|
|
63
41
|
}
|
|
42
|
+
if (parts.length === 1)
|
|
43
|
+
return parts[0];
|
|
44
|
+
const out = new Uint8Array(bytes);
|
|
45
|
+
let offset = 0;
|
|
46
|
+
for (const part of parts) {
|
|
47
|
+
out.set(part, offset);
|
|
48
|
+
offset += part.byteLength;
|
|
49
|
+
}
|
|
50
|
+
return out;
|
|
51
|
+
}
|
|
52
|
+
function positiveTimeout(value, name) {
|
|
53
|
+
if (!Number.isFinite(value) || value <= 0 || value > 2_147_483_647) {
|
|
54
|
+
throw new TypeError(`${name} must be positive and at most 2147483647`);
|
|
55
|
+
}
|
|
56
|
+
return value;
|
|
57
|
+
}
|
|
58
|
+
function byteLimit(value, name) {
|
|
59
|
+
if (!Number.isSafeInteger(value) || value <= 0)
|
|
60
|
+
throw new TypeError(`${name} must be a positive safe integer`);
|
|
61
|
+
return value;
|
|
64
62
|
}
|
|
65
63
|
/** Reusable fetch client. Server root URL, optional proxy prefix, without /v1.
|
|
66
64
|
* No automatic retries. AbortSignal cancels a request; timeout includes body reading.
|
|
@@ -96,15 +94,24 @@ export class BasalClient {
|
|
|
96
94
|
this.headers.Authorization = `Bearer ${options.apiKey}`;
|
|
97
95
|
}
|
|
98
96
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
97
|
+
request(method, path, body, signal) {
|
|
98
|
+
return this.call({ method, path, json: body, signal });
|
|
99
|
+
}
|
|
100
|
+
async call(call) {
|
|
101
|
+
// Serialization and option errors are local input errors, not failures of the HTTP transport.
|
|
102
|
+
const encoded = call.json === undefined ? undefined : JSON.stringify(call.json);
|
|
103
|
+
const timeoutMs = call.timeoutMs === undefined ? this.timeoutMs : positiveTimeout(call.timeoutMs, "timeoutMs");
|
|
104
|
+
const limit = call.maxBytes === undefined ? this.maxResponseBytes : byteLimit(call.maxBytes, "maxBytes");
|
|
105
|
+
const headers = { ...this.headers, ...call.headers };
|
|
106
|
+
if (encoded !== undefined)
|
|
107
|
+
headers["Content-Type"] = "application/json";
|
|
108
|
+
const { signal } = call;
|
|
102
109
|
const controller = new AbortController();
|
|
103
110
|
const abort = () => controller.abort(signal?.reason);
|
|
104
111
|
signal?.addEventListener("abort", abort, { once: true });
|
|
105
112
|
if (signal?.aborted)
|
|
106
113
|
abort();
|
|
107
|
-
const timer = setTimeout(() => controller.abort(new Error("Basal request timed out")),
|
|
114
|
+
const timer = setTimeout(() => controller.abort(new Error("Basal request timed out")), timeoutMs);
|
|
108
115
|
let onAbort = () => { };
|
|
109
116
|
const aborted = new Promise((_, reject) => {
|
|
110
117
|
onAbort = () => reject(new BasalTransportError("Basal HTTP request aborted", { cause: controller.signal.reason }));
|
|
@@ -114,24 +121,39 @@ export class BasalClient {
|
|
|
114
121
|
});
|
|
115
122
|
const work = async () => {
|
|
116
123
|
let response;
|
|
117
|
-
let
|
|
124
|
+
let content;
|
|
125
|
+
let raw = "";
|
|
118
126
|
try {
|
|
119
|
-
|
|
120
|
-
method, headers:
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
127
|
+
const init = {
|
|
128
|
+
method: call.method, headers, signal: controller.signal, redirect: "error", credentials: "omit", cache: "no-store",
|
|
129
|
+
};
|
|
130
|
+
if (encoded !== undefined)
|
|
131
|
+
init.body = encoded;
|
|
132
|
+
else if (call.body instanceof Blob)
|
|
133
|
+
init.body = call.body;
|
|
134
|
+
else if (call.body !== undefined) {
|
|
135
|
+
// Streamed request bodies require half-duplex fetch (Node.js, Chromium over HTTP/2).
|
|
136
|
+
init.body = call.body(controller.signal);
|
|
137
|
+
init.duplex = "half";
|
|
138
|
+
}
|
|
139
|
+
response = await this.fetch(this.baseUrl + call.path, init);
|
|
124
140
|
if (response.redirected || ([301, 302, 303, 307, 308].includes(response.status) && response.headers.has("Location"))) {
|
|
125
141
|
await response.body?.cancel().catch(() => { });
|
|
126
142
|
throw new BasalTransportError("Basal transport returned a redirect");
|
|
127
143
|
}
|
|
128
|
-
|
|
144
|
+
// A per-download cap limits successful artifact bytes. Error envelopes retain the client's normal cap
|
|
145
|
+
// so a small artifact cap does not hide an actionable 404/429 and its Retry-After.
|
|
146
|
+
content = await readBody(response, response.ok ? limit : this.maxResponseBytes, controller.signal);
|
|
147
|
+
if (!call.bytes || !response.ok)
|
|
148
|
+
raw = new TextDecoder("utf-8", { fatal: true }).decode(content);
|
|
129
149
|
}
|
|
130
150
|
catch (cause) {
|
|
131
151
|
if (cause instanceof BasalError)
|
|
132
152
|
throw cause;
|
|
133
153
|
throw new BasalTransportError("Basal HTTP transport failed", { cause });
|
|
134
154
|
}
|
|
155
|
+
if (call.bytes && response.ok)
|
|
156
|
+
return content;
|
|
135
157
|
let data;
|
|
136
158
|
try {
|
|
137
159
|
data = JSON.parse(raw);
|
|
@@ -157,6 +179,9 @@ export class BasalClient {
|
|
|
157
179
|
controller.signal.removeEventListener("abort", onAbort);
|
|
158
180
|
clearTimeout(timer);
|
|
159
181
|
signal?.removeEventListener("abort", abort);
|
|
182
|
+
// An answer can arrive before a streamed upload is fully sent (e.g. 413); stop its source.
|
|
183
|
+
if (typeof call.body === "function")
|
|
184
|
+
controller.abort(new Error("Basal request finished"));
|
|
160
185
|
}
|
|
161
186
|
}
|
|
162
187
|
async decide(state, questionsOrSignal, options = {}) {
|
|
@@ -191,4 +216,45 @@ export class BasalClient {
|
|
|
191
216
|
return typed;
|
|
192
217
|
}
|
|
193
218
|
models(signal) { return this.request("GET", "v1/models", undefined, signal); }
|
|
219
|
+
/** Upload a JSONL input file (`purpose=batch`) from caller-owned bytes, a Blob or a stream; never fetches URLs. */
|
|
220
|
+
async uploadFile(request, options = {}) {
|
|
221
|
+
const form = uploadForm(request);
|
|
222
|
+
return await this.call({
|
|
223
|
+
method: "POST", path: "v1/files", body: form.body, headers: { "Content-Type": form.contentType },
|
|
224
|
+
signal: options.signal, timeoutMs: options.timeoutMs,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
async listFiles(options = {}) {
|
|
228
|
+
return await this.call({ method: "GET", path: listPath("v1/files", options), signal: options.signal });
|
|
229
|
+
}
|
|
230
|
+
async getFile(fileId, options = {}) {
|
|
231
|
+
return await this.call({ method: "GET", path: resourcePath("v1/files", fileId), signal: options.signal });
|
|
232
|
+
}
|
|
233
|
+
async deleteFile(fileId, options = {}) {
|
|
234
|
+
return await this.call({ method: "DELETE", path: resourcePath("v1/files", fileId), signal: options.signal });
|
|
235
|
+
}
|
|
236
|
+
/** Raw file bytes (JSONL), buffered up to `maxBytes` (default `maxResponseBytes`); never JSON-parsed. */
|
|
237
|
+
async downloadFile(fileId, options = {}) {
|
|
238
|
+
return await this.call({
|
|
239
|
+
method: "GET", path: resourcePath("v1/files", fileId, "/content"), bytes: true,
|
|
240
|
+
headers: { Accept: "application/jsonl, application/json" },
|
|
241
|
+
signal: options.signal, timeoutMs: options.timeoutMs, maxBytes: options.maxBytes,
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
async createBatch(request, options = {}) {
|
|
245
|
+
const headers = idempotencyHeader(options.idempotencyKey);
|
|
246
|
+
return await this.call({ method: "POST", path: "v1/batches", json: createBody(request), headers, signal: options.signal });
|
|
247
|
+
}
|
|
248
|
+
async listBatches(options = {}) {
|
|
249
|
+
const { after, limit } = options;
|
|
250
|
+
const query = { ...(after === undefined ? {} : { after }), ...(limit === undefined ? {} : { limit }) };
|
|
251
|
+
return await this.call({ method: "GET", path: listPath("v1/batches", query), signal: options.signal });
|
|
252
|
+
}
|
|
253
|
+
async getBatch(batchId, options = {}) {
|
|
254
|
+
return await this.call({ method: "GET", path: resourcePath("v1/batches", batchId), signal: options.signal });
|
|
255
|
+
}
|
|
256
|
+
/** Request cancellation; the batch moves to `cancelling` and later `cancelled`. Does not wait. */
|
|
257
|
+
async cancelBatch(batchId, options = {}) {
|
|
258
|
+
return await this.call({ method: "POST", path: resourcePath("v1/batches", batchId, "/cancel"), signal: options.signal });
|
|
259
|
+
}
|
|
194
260
|
}
|