@posthog/ai 8.15.2 → 8.16.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,1547 @@
1
+ import { randomUUID } from "node:crypto";
2
+ //#region src/evaluations/serialization.ts
3
+ function object(value) {
4
+ if (value === null || typeof value !== "object" || Array.isArray(value)) throw new TypeError("Expected an object.");
5
+ const proto = Object.getPrototypeOf(value);
6
+ if (proto !== Object.prototype && proto !== null) throw new TypeError("Expected a plain object.");
7
+ return value;
8
+ }
9
+ function text(value) {
10
+ if (typeof value !== "string") throw new TypeError("Expected a string.");
11
+ for (let i = 0; i < value.length; i++) {
12
+ const c = value.charCodeAt(i);
13
+ if (c === 0) throw new TypeError("Strings cannot contain null characters.");
14
+ if (c >= 55296 && c <= 56319) {
15
+ const next = value.charCodeAt(++i);
16
+ if (!(next >= 56320 && next <= 57343)) throw new TypeError("Strings must contain valid Unicode.");
17
+ } else if (c >= 56320 && c <= 57343) throw new TypeError("Strings must contain valid Unicode.");
18
+ }
19
+ return value;
20
+ }
21
+ function identifier(value, max = 255) {
22
+ const result = text(value);
23
+ if (!result || [...result].length > max) throw new TypeError(`Expected a nonempty identifier of at most ${max} characters.`);
24
+ return result;
25
+ }
26
+ function integer(value, minimum = 1, maximum = Number.MAX_SAFE_INTEGER) {
27
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < minimum || value > maximum) throw new TypeError(`Expected a safe integer between ${minimum} and ${maximum}.`);
28
+ return value;
29
+ }
30
+ function count(value) {
31
+ if (typeof value === "bigint") {
32
+ if (value < 0n || value > 9223372036854775807n) throw new TypeError("Count must fit a nonnegative signed 64-bit integer.");
33
+ return value <= BigInt(Number.MAX_SAFE_INTEGER) ? Number(value) : value;
34
+ }
35
+ return integer(value, 0);
36
+ }
37
+ function boolean(value) {
38
+ if (typeof value !== "boolean") throw new TypeError("Expected a boolean.");
39
+ return value;
40
+ }
41
+ function uuid(value) {
42
+ const raw = text(value).replace(/^urn:uuid:/i, "").replace(/^\{(.*)\}$/, "$1").replace(/-/g, "");
43
+ if (!/^[0-9a-f]{32}$/i.test(raw)) throw new TypeError("Expected a UUID.");
44
+ return `${raw.slice(0, 8)}-${raw.slice(8, 12)}-${raw.slice(12, 16)}-${raw.slice(16, 20)}-${raw.slice(20)}`.toLowerCase();
45
+ }
46
+ function timestamp(value) {
47
+ if (value instanceof Date) {
48
+ if (!Number.isFinite(value.getTime())) throw new TypeError("Expected a valid timestamp.");
49
+ return value.toISOString();
50
+ }
51
+ const raw = text(value);
52
+ const match = /^(\d{4}-\d{2}-\d{2})[T ](\d{2}:\d{2}:\d{2})(\.\d+)?(Z|[+-]\d{2}:\d{2})$/.exec(raw);
53
+ if (!match) throw new TypeError("Timestamps must include a timezone.");
54
+ const date = /* @__PURE__ */ new Date(`${match[1]}T${match[2]}${match[4]}`);
55
+ const day = /* @__PURE__ */ new Date(`${match[1]}T00:00:00Z`);
56
+ if (!Number.isFinite(date.getTime()) || !Number.isFinite(day.getTime()) || day.toISOString().slice(0, 10) !== match[1] || match[2].startsWith("24:")) throw new TypeError("Expected a valid timestamp.");
57
+ return date.toISOString().replace(".000Z", `${match[3] ?? ".000"}Z`);
58
+ }
59
+ function json(value, depth = 1, ancestors = /* @__PURE__ */ new Set()) {
60
+ if (value === null || typeof value === "boolean") return value;
61
+ if (typeof value === "string") return text(value);
62
+ if (typeof value === "number") {
63
+ if (!Number.isFinite(value)) throw new TypeError("JSON numbers must be finite.");
64
+ return value;
65
+ }
66
+ if (typeof value !== "object") throw new TypeError("Expected JSON values without automatic conversion.");
67
+ if (depth > 32 || ancestors.has(value)) throw new TypeError("JSON must be acyclic and at most 32 containers deep.");
68
+ ancestors.add(value);
69
+ try {
70
+ if (Array.isArray(value)) {
71
+ if (Reflect.ownKeys(value).some((key) => key !== "length" && (typeof key !== "string" || !/^(0|[1-9]\d*)$/.test(key) || Number(key) >= value.length))) throw new TypeError("Unexpected array property.");
72
+ const result = [];
73
+ for (let i = 0; i < value.length; i++) {
74
+ const d = Object.getOwnPropertyDescriptor(value, String(i));
75
+ if (!d || !("value" in d)) throw new TypeError("Sparse arrays and accessors are not JSON.");
76
+ result.push(json(d.value, depth + 1, ancestors));
77
+ }
78
+ return result;
79
+ }
80
+ const source = object(value);
81
+ return Object.fromEntries(Reflect.ownKeys(source).map((key) => {
82
+ if (typeof key !== "string") throw new TypeError("JSON keys must be strings.");
83
+ text(key);
84
+ const d = Object.getOwnPropertyDescriptor(source, key);
85
+ if (!("value" in d) || !d.enumerable) throw new TypeError("JSON objects must contain enumerable data properties.");
86
+ return [key, json(d.value, depth + 1, ancestors)];
87
+ }));
88
+ } finally {
89
+ ancestors.delete(value);
90
+ }
91
+ }
92
+ function metadata(value) {
93
+ return json(object(value));
94
+ }
95
+ /** Convert losslessly decoded response integers only when JavaScript can retain their value. */
96
+ function responseJSON(value) {
97
+ function convert(current) {
98
+ if (typeof current === "bigint") {
99
+ const number = Number(current);
100
+ if (!Number.isFinite(number) || BigInt(number) !== current) throw new TypeError("Response JSON integer cannot be represented exactly.");
101
+ return number;
102
+ }
103
+ if (Array.isArray(current)) return current.map(convert);
104
+ if (current !== null && typeof current === "object") return Object.fromEntries(Object.entries(object(current)).map(([key, v]) => [key, convert(v)]));
105
+ return current;
106
+ }
107
+ return json(convert(value));
108
+ }
109
+ function responseMetadata(value) {
110
+ object(value);
111
+ return responseJSON(value);
112
+ }
113
+ /** Internal codec: bigint is allowed only after validation of designated count fields. */
114
+ function encode(value) {
115
+ if (typeof value === "bigint") return value.toString();
116
+ if (typeof value === "number" && Number.isInteger(value)) return BigInt(value).toString();
117
+ if (Array.isArray(value)) return `[${value.map(encode).join(",")}]`;
118
+ if (value !== null && typeof value === "object") return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${encode(value[key])}`).join(",")}}`;
119
+ const encoded = JSON.stringify(value);
120
+ if (encoded === void 0) throw new TypeError("Cannot encode undefined.");
121
+ return encoded;
122
+ }
123
+ /** Preserve integer tokens before JSON.parse can round signed-64-bit counters. */
124
+ function decode(source) {
125
+ JSON.parse(source);
126
+ const tokens = source.match(/"(?:\\[\s\S]|[^"\\])*"|-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?|true|false|null|[{}[\]:,]/g);
127
+ let i = 0;
128
+ function read(depth) {
129
+ if (depth > 256) throw new TypeError("Response JSON nesting is too deep.");
130
+ const token = tokens[i++];
131
+ if (token === "{") {
132
+ const entries = [];
133
+ while (tokens[i] !== "}") {
134
+ const key = JSON.parse(tokens[i++]);
135
+ i++;
136
+ entries.push([key, read(depth + 1)]);
137
+ if (tokens[i] !== ",") break;
138
+ i++;
139
+ }
140
+ i++;
141
+ return Object.fromEntries(entries);
142
+ }
143
+ if (token === "[") {
144
+ const values = [];
145
+ while (tokens[i] !== "]") {
146
+ values.push(read(depth + 1));
147
+ if (tokens[i] !== ",") break;
148
+ i++;
149
+ }
150
+ i++;
151
+ return values;
152
+ }
153
+ if (/^-?\d+$/.test(token)) {
154
+ const exact = BigInt(token);
155
+ if (exact > BigInt(Number.MAX_SAFE_INTEGER) || exact < BigInt(Number.MIN_SAFE_INTEGER)) return exact;
156
+ }
157
+ return JSON.parse(token);
158
+ }
159
+ return read(0);
160
+ }
161
+ function copy(value) {
162
+ return structuredClone(value);
163
+ }
164
+ function bytes(value) {
165
+ return new TextEncoder().encode(value).length;
166
+ }
167
+ function keys(value, allowed) {
168
+ for (const key of Reflect.ownKeys(value)) {
169
+ if (typeof key !== "string" || !allowed.includes(key)) throw new TypeError("Unsupported option.");
170
+ if (!("value" in Object.getOwnPropertyDescriptor(value, key))) throw new TypeError("Options cannot contain accessors.");
171
+ }
172
+ }
173
+ function defined(value) {
174
+ return Object.fromEntries(Object.entries(value).filter(([, v]) => v !== void 0));
175
+ }
176
+ function nullable(value, parse) {
177
+ return value === null ? null : parse(value);
178
+ }
179
+ function tags(value, limit = 100) {
180
+ if (!Array.isArray(value) || value.length > limit) throw new TypeError(`Expected at most ${limit} tag names.`);
181
+ for (let i = 0; i < value.length; i++) if (!Object.hasOwn(value, i)) throw new TypeError("Tag lists cannot be sparse.");
182
+ return value.map((v) => {
183
+ const s = text(v);
184
+ identifier(s.trim());
185
+ return s;
186
+ });
187
+ }
188
+ function responseTags(value) {
189
+ if (!Array.isArray(value)) throw new TypeError("Expected tags.");
190
+ return value.map(text);
191
+ }
192
+ function tagParams(value, match = "any") {
193
+ if (match !== "any" && match !== "all") throw new TypeError("Unsupported tag matching mode.");
194
+ return defined({
195
+ tags: value === void 0 ? void 0 : encode(tags(value, 20)),
196
+ tag_match: value !== void 0 || match !== "any" ? match : void 0
197
+ });
198
+ }
199
+ function pageParams(limit, offset, maximum = 100) {
200
+ return {
201
+ limit: integer(limit, 1, maximum),
202
+ offset: integer(offset, 0)
203
+ };
204
+ }
205
+ function nextOffset(value, offset) {
206
+ if (value === null) return null;
207
+ const params = new URL(text(value), "https://pagination.invalid").searchParams.getAll("offset");
208
+ if (params.length !== 1 || !/^\d+$/.test(params[0])) throw new TypeError("Invalid page continuation.");
209
+ return integer(Number(params[0]), offset + 1);
210
+ }
211
+ function page(data, parse, offset) {
212
+ if (!Array.isArray(data.results)) throw new TypeError("Expected page results.");
213
+ const results = data.results.map((v) => parse(object(v)));
214
+ const next = nextOffset(data.next, offset);
215
+ if (next !== null && results.length === 0) throw new TypeError("Empty page has a continuation.");
216
+ return {
217
+ count: count(data.count),
218
+ results,
219
+ nextOffset: next
220
+ };
221
+ }
222
+ //#endregion
223
+ //#region src/evaluations/errors.ts
224
+ /** Native cancellation with the prepared creation fields needed for recovery. */
225
+ var ExperimentCreationAbortedError = class extends DOMException {
226
+ #cause;
227
+ #submission;
228
+ constructor(cause, submission) {
229
+ super("Offline evaluation creation was cancelled.", cause instanceof DOMException ? cause.name : "AbortError");
230
+ this.#cause = cause;
231
+ this.#submission = copy(submission);
232
+ Object.defineProperty(this, "cause", {
233
+ get: () => this.#cause,
234
+ enumerable: false
235
+ });
236
+ }
237
+ get submission() {
238
+ return copy(this.#submission);
239
+ }
240
+ };
241
+ /** An operation did not receive a valid acknowledgment. Inspect persistence before recovery. */
242
+ var EvaluationAPIError = class extends Error {
243
+ #errors;
244
+ #submission;
245
+ constructor(fields = {}) {
246
+ super(`Offline evaluation request failed (${fields.status === void 0 ? "no acknowledgment" : `HTTP ${fields.status}`}).`);
247
+ this.name = "EvaluationAPIError";
248
+ this.status = fields.status;
249
+ this.code = fields.code;
250
+ this.detail = fields.detail === void 0 ? void 0 : copy(fields.detail);
251
+ this.attr = fields.attr;
252
+ this.#errors = fields.errors === void 0 ? void 0 : copy(fields.errors);
253
+ this.response = fields.response === void 0 ? void 0 : copy(fields.response);
254
+ this.retryAfter = fields.retryAfter;
255
+ this.persistence = fields.persistence ?? "unknown";
256
+ this.#submission = fields.submission === void 0 ? void 0 : copy(fields.submission);
257
+ for (const key of ["detail", "response"]) Object.defineProperty(this, key, { enumerable: false });
258
+ Object.defineProperty(this, "errors", {
259
+ get: () => this.#errors === void 0 ? void 0 : copy(this.#errors),
260
+ enumerable: false
261
+ });
262
+ }
263
+ get submission() {
264
+ return this.#submission === void 0 ? void 0 : copy(this.#submission);
265
+ }
266
+ };
267
+ function parseResponse(data, parser) {
268
+ try {
269
+ return parser(data);
270
+ } catch {
271
+ throw new EvaluationAPIError({
272
+ code: "invalid_response",
273
+ response: data
274
+ });
275
+ }
276
+ }
277
+ //#endregion
278
+ //#region src/evaluations/datasets.ts
279
+ function audit(data, project) {
280
+ if (integer(data.team_id) !== project) throw new TypeError("Response belongs to another project.");
281
+ return {
282
+ createdAt: timestamp(data.created_at),
283
+ createdBy: nullable(data.created_by, responseMetadata),
284
+ teamId: project
285
+ };
286
+ }
287
+ function dataset(data, project, id) {
288
+ const identity = uuid(data.id);
289
+ if (id !== void 0 && identity !== id) throw new TypeError("Mismatched dataset.");
290
+ if (data.current_revision === null !== (data.current_revision_id === null)) throw new TypeError("Incomplete revision reference.");
291
+ return {
292
+ ...audit(data, project),
293
+ id: identity,
294
+ name: text(data.name),
295
+ description: text(data.description),
296
+ metadata: responseMetadata(data.metadata),
297
+ tags: responseTags(data.tags === void 0 ? [] : data.tags),
298
+ archived: boolean(data.archived),
299
+ currentRevision: nullable(data.current_revision, integer),
300
+ currentRevisionId: nullable(data.current_revision_id, uuid),
301
+ updatedAt: nullable(data.updated_at, timestamp)
302
+ };
303
+ }
304
+ function revision(data, project, datasetId) {
305
+ if (uuid(data.dataset_id) !== datasetId) throw new TypeError("Mismatched revision dataset.");
306
+ return {
307
+ ...audit(data, project),
308
+ id: uuid(data.id),
309
+ datasetId,
310
+ revision: integer(data.revision)
311
+ };
312
+ }
313
+ function item(data, project, scope = {}) {
314
+ const id = uuid(data.id), datasetId = uuid(data.dataset), introduced = integer(data.dataset_revision), archived = boolean(data.archived);
315
+ if (scope.id !== void 0 && scope.id !== id || scope.datasetId !== void 0 && scope.datasetId !== datasetId || scope.revision !== void 0 && introduced > scope.revision || scope.archived !== void 0 && archived !== scope.archived) throw new TypeError("Item is outside requested scope.");
316
+ if (data.input === null) throw new TypeError("Dataset input cannot be null.");
317
+ return {
318
+ ...audit(data, project),
319
+ id,
320
+ datasetId,
321
+ datasetRevision: introduced,
322
+ archived,
323
+ clientItemId: nullable(data.client_item_id, identifier),
324
+ version: integer(data.version),
325
+ versionId: uuid(data.version_id),
326
+ datasetRevisionId: uuid(data.dataset_revision_id),
327
+ input: responseJSON(data.input),
328
+ expectedOutput: responseJSON(data.expected_output),
329
+ sourceOutput: responseJSON(data.source_output),
330
+ metadata: responseMetadata(data.metadata),
331
+ sourceTraceId: nullable(data.source_trace_id, identifier),
332
+ sourceEventId: nullable(data.source_event_id, identifier),
333
+ sourceTimestamp: nullable(data.source_timestamp, timestamp),
334
+ updatedAt: nullable(data.updated_at, timestamp),
335
+ versionCreatedAt: timestamp(data.version_created_at),
336
+ versionCreatedBy: nullable(data.version_created_by, responseMetadata)
337
+ };
338
+ }
339
+ function body(fields) {
340
+ const data = defined(fields);
341
+ for (const key of ["name", "description"]) if (Object.hasOwn(data, key)) {
342
+ const value = text(data[key]), maximum = key === "name" ? 400 : 1e4;
343
+ if ([...value].length > maximum || key === "name" && !value.trim()) throw new TypeError("Invalid dataset metadata.");
344
+ }
345
+ if (Object.hasOwn(data, "input") && data.input === null) throw new TypeError("Dataset input cannot be null.");
346
+ if (Object.hasOwn(data, "metadata")) metadata(data.metadata);
347
+ if (Object.hasOwn(data, "tags")) data.tags = tags(data.tags);
348
+ return encode(json(data));
349
+ }
350
+ const METADATA_KEYS = [
351
+ "name",
352
+ "description",
353
+ "metadata",
354
+ "tags"
355
+ ];
356
+ const ITEM_KEYS = [
357
+ "input",
358
+ "expectedOutput",
359
+ "sourceOutput",
360
+ "metadata",
361
+ "clientItemId",
362
+ "sourceTraceId",
363
+ "sourceEventId",
364
+ "sourceTimestamp"
365
+ ];
366
+ /** Dataset management. Writes require a personal key with dataset:write. */
367
+ var Datasets = class {
368
+ #transport;
369
+ #base;
370
+ #items;
371
+ constructor(transport) {
372
+ this.#transport = transport;
373
+ this.#base = `/api/projects/${transport.projectId}/datasets/`;
374
+ this.#items = `/api/projects/${transport.projectId}/dataset_items/`;
375
+ }
376
+ async #request(method, path, parse, options) {
377
+ return parseResponse(await this.#transport.request(method, path, options), parse);
378
+ }
379
+ async create(options, request = {}) {
380
+ keys(options, METADATA_KEYS);
381
+ if (options.name === void 0) throw new TypeError("Dataset name is required.");
382
+ return this.#request("POST", this.#base, (data) => dataset(data, this.#transport.projectId), {
383
+ ...request,
384
+ body: body({ ...options })
385
+ });
386
+ }
387
+ async get(datasetId, request = {}) {
388
+ const id = uuid(datasetId);
389
+ return this.#request("GET", `${this.#base}${id}/`, (data) => dataset(data, this.#transport.projectId, id), request);
390
+ }
391
+ async list(options = {}) {
392
+ const { limit = 50, offset = 0, archived = false, orderBy = "-created_at" } = options;
393
+ if (![
394
+ "created_at",
395
+ "-created_at",
396
+ "updated_at",
397
+ "-updated_at"
398
+ ].includes(orderBy)) throw new TypeError("Invalid dataset order.");
399
+ const params = {
400
+ ...pageParams(limit, offset),
401
+ ...tagParams(options.tags, options.tagMatch),
402
+ archived: String(boolean(archived)),
403
+ order_by: orderBy
404
+ };
405
+ if (options.search !== void 0 && !text(options.search).trim()) throw new TypeError("Search cannot be blank.");
406
+ return this.#request("GET", this.#base, (data) => page(data, (row) => dataset(row, this.#transport.projectId), offset), {
407
+ signal: options.signal,
408
+ params: {
409
+ ...params,
410
+ ...defined({ search: options.search })
411
+ }
412
+ });
413
+ }
414
+ async update(datasetId, options, request = {}) {
415
+ const id = uuid(datasetId);
416
+ keys(options, METADATA_KEYS);
417
+ return this.#request("PATCH", `${this.#base}${id}/`, (data) => dataset(data, this.#transport.projectId, id), {
418
+ ...request,
419
+ body: body({ ...options })
420
+ });
421
+ }
422
+ async archive(datasetId, request = {}) {
423
+ return this.#state(datasetId, "archive", request);
424
+ }
425
+ async restore(datasetId, request = {}) {
426
+ return this.#state(datasetId, "restore", request);
427
+ }
428
+ async #state(datasetId, action, request) {
429
+ const id = uuid(datasetId);
430
+ return this.#request("POST", `${this.#base}${id}/${action}/`, (data) => dataset(data, this.#transport.projectId, id), {
431
+ ...request,
432
+ body: "{}"
433
+ });
434
+ }
435
+ async listRevisions(datasetId, options = {}) {
436
+ const id = uuid(datasetId), { limit = 50, offset = 0 } = options;
437
+ return this.#request("GET", `${this.#base}${id}/revisions/`, (data) => page(data, (row) => revision(row, this.#transport.projectId, id), offset), {
438
+ signal: options.signal,
439
+ params: pageParams(limit, offset)
440
+ });
441
+ }
442
+ async listItems(datasetId, options = {}) {
443
+ const id = uuid(datasetId), { limit = 25, offset = 0, archived = false, revision: selectedRevision } = options;
444
+ const params = {
445
+ ...pageParams(limit, offset, 25),
446
+ dataset: id,
447
+ archived: String(boolean(archived)),
448
+ ...defined({ revision: selectedRevision === void 0 ? void 0 : integer(selectedRevision) })
449
+ };
450
+ return this.#request("GET", this.#items, (data) => page(data, (row) => item(row, this.#transport.projectId, {
451
+ datasetId: id,
452
+ revision: selectedRevision,
453
+ archived
454
+ }), offset), {
455
+ signal: options.signal,
456
+ params
457
+ });
458
+ }
459
+ async getItem(itemId, options = {}) {
460
+ const id = uuid(itemId), selectedRevision = options.revision;
461
+ const params = defined({ revision: selectedRevision === void 0 ? void 0 : integer(selectedRevision) });
462
+ return this.#request("GET", `${this.#items}${id}/`, (data) => item(data, this.#transport.projectId, {
463
+ id,
464
+ revision: selectedRevision
465
+ }), {
466
+ signal: options.signal,
467
+ params
468
+ });
469
+ }
470
+ async createItem(datasetId, options, request = {}) {
471
+ const id = uuid(datasetId);
472
+ keys(options, ITEM_KEYS);
473
+ if (options.input === void 0) throw new TypeError("Dataset input is required.");
474
+ const clientId = options.clientItemId == null ? null : identifier(options.clientItemId);
475
+ const trace = options.sourceTraceId == null ? null : identifier(options.sourceTraceId);
476
+ const event = options.sourceEventId == null ? null : identifier(options.sourceEventId);
477
+ const time = options.sourceTimestamp == null ? null : timestamp(options.sourceTimestamp);
478
+ if (trace === null !== (time === null) || event !== null && trace === null) throw new TypeError("Source provenance requires a trace and timezone-qualified timestamp.");
479
+ const prepared = body({
480
+ dataset: id,
481
+ input: options.input,
482
+ expected_output: options.expectedOutput,
483
+ source_output: options.sourceOutput,
484
+ metadata: options.metadata,
485
+ client_item_id: clientId,
486
+ source_trace_id: trace,
487
+ source_event_id: event,
488
+ source_timestamp: time
489
+ });
490
+ return this.#request("POST", this.#items, (data) => item(data, this.#transport.projectId, {
491
+ datasetId: id,
492
+ archived: false
493
+ }), {
494
+ ...request,
495
+ body: prepared,
496
+ retrySafe: clientId !== null
497
+ });
498
+ }
499
+ async updateItem(itemId, options, request = {}) {
500
+ const id = uuid(itemId);
501
+ keys(options, [
502
+ "baseVersion",
503
+ "input",
504
+ "expectedOutput",
505
+ "metadata"
506
+ ]);
507
+ const prepared = body({
508
+ base_version: integer(options.baseVersion),
509
+ input: options.input,
510
+ expected_output: options.expectedOutput,
511
+ metadata: options.metadata
512
+ });
513
+ return this.#request("PATCH", `${this.#items}${id}/`, (data) => item(data, this.#transport.projectId, {
514
+ id,
515
+ archived: false
516
+ }), {
517
+ ...request,
518
+ body: prepared
519
+ });
520
+ }
521
+ async archiveItem(itemId, options, request = {}) {
522
+ keys(options, ["baseVersion"]);
523
+ return this.#itemState(itemId, "archive", { base_version: integer(options.baseVersion) }, request);
524
+ }
525
+ async restoreItem(itemId, options, request = {}) {
526
+ keys(options, ["baseVersion", "sourceVersion"]);
527
+ return this.#itemState(itemId, "restore", {
528
+ base_version: integer(options.baseVersion),
529
+ source_version: options.sourceVersion === void 0 ? null : integer(options.sourceVersion)
530
+ }, request);
531
+ }
532
+ async #itemState(itemId, action, fields, request) {
533
+ const id = uuid(itemId);
534
+ return this.#request("POST", `${this.#items}${id}/${action}/`, (data) => item(data, this.#transport.projectId, {
535
+ id,
536
+ archived: action === "archive"
537
+ }), {
538
+ ...request,
539
+ body: body(fields)
540
+ });
541
+ }
542
+ async listItemVersions(itemId, options = {}) {
543
+ const id = uuid(itemId), { limit = 25, offset = 0 } = options;
544
+ return this.#request("GET", `${this.#items}${id}/versions/`, (data) => page(data, (row) => item(row, this.#transport.projectId, { id }), offset), {
545
+ signal: options.signal,
546
+ params: pageParams(limit, offset)
547
+ });
548
+ }
549
+ async snapshot(datasetId, options = {}) {
550
+ const selectedRevision = options.revision;
551
+ if (selectedRevision !== void 0) integer(selectedRevision);
552
+ const data = await this.get(datasetId, options), selected = selectedRevision ?? data.currentRevision;
553
+ if (selected === null) throw new TypeError("Dataset has no revision. Add an item first.");
554
+ if (data.currentRevision === null || selected > data.currentRevision) throw new TypeError("Dataset revision does not exist.");
555
+ if (selected === data.currentRevision && data.currentRevisionId !== null) return new DatasetSnapshot(this, data, selected, data.currentRevisionId);
556
+ const found = (await this.listRevisions(data.id, {
557
+ limit: 1,
558
+ offset: data.currentRevision - selected,
559
+ signal: options.signal
560
+ })).results.find((r) => r.revision === selected);
561
+ if (found) return new DatasetSnapshot(this, data, selected, found.id);
562
+ let offset = 0;
563
+ for (;;) {
564
+ const revisions = await this.listRevisions(data.id, {
565
+ limit: 100,
566
+ offset,
567
+ signal: options.signal
568
+ });
569
+ const match = revisions.results.find((r) => r.revision === selected);
570
+ if (match) return new DatasetSnapshot(this, data, selected, match.id);
571
+ if (revisions.nextOffset === null || revisions.results.at(-1).revision < selected) throw new TypeError("Dataset revision does not exist.");
572
+ offset = revisions.nextOffset;
573
+ }
574
+ }
575
+ };
576
+ /** A repeatable, lazy read of active items at one immutable revision. Keep its client open. */
577
+ var DatasetSnapshot = class {
578
+ #datasets;
579
+ #dataset;
580
+ constructor(datasets, dataset, revisionNumber, revisionId) {
581
+ this.#datasets = datasets;
582
+ this.#dataset = structuredClone(dataset);
583
+ this.revision = revisionNumber;
584
+ this.revisionId = revisionId;
585
+ Object.freeze(this);
586
+ }
587
+ get dataset() {
588
+ return structuredClone(this.#dataset);
589
+ }
590
+ async *iterItems(options = {}) {
591
+ const limit = integer(options.pageSize ?? 25, 1, 25);
592
+ let offset = 0;
593
+ for (;;) {
594
+ const result = await this.#datasets.listItems(this.#dataset.id, {
595
+ revision: this.revision,
596
+ archived: false,
597
+ limit,
598
+ offset,
599
+ signal: options.signal
600
+ });
601
+ for (const row of result.results) {
602
+ options.signal?.throwIfAborted();
603
+ yield row;
604
+ }
605
+ if (result.nextOffset === null) return;
606
+ offset = result.nextOffset;
607
+ }
608
+ }
609
+ [Symbol.asyncIterator]() {
610
+ return this.iterItems();
611
+ }
612
+ };
613
+ //#endregion
614
+ //#region src/evaluations/declarations.ts
615
+ const ITEM_FIELDS = {
616
+ input: "input",
617
+ output: "output",
618
+ expectedOutput: "expected_output",
619
+ metadata: "metadata"
620
+ };
621
+ const RESULT_FIELDS = {
622
+ reasoning: "reasoning",
623
+ errorMessage: "error_message",
624
+ metadata: "metadata"
625
+ };
626
+ const ITEM_IDS = {
627
+ caseKey: "case_key",
628
+ trial: "trial",
629
+ datasetItemIdentifier: "dataset_item_identifier",
630
+ datasetItemVersionIdentifier: "dataset_item_version_identifier",
631
+ applicationTraceId: "application_trace_id"
632
+ };
633
+ const items = /* @__PURE__ */ new WeakMap();
634
+ const results = /* @__PURE__ */ new WeakMap();
635
+ function payload(options, fields, limit) {
636
+ const supplied = Object.keys(fields).filter((key) => options[key] !== void 0);
637
+ if (options.payload !== void 0 && supplied.length) throw new TypeError("Supply payload or individual fields, not both.");
638
+ if (options.payload === void 0 && !supplied.length) return void 0;
639
+ const source = options.payload === void 0 ? Object.fromEntries(supplied.map((key) => [key, options[key]])) : object(options.payload);
640
+ keys(source, Object.keys(fields));
641
+ const snapshot = json(Object.fromEntries(Object.entries(source).map(([key, v]) => {
642
+ if (key === "metadata" && v !== null) metadata(v);
643
+ if ((key === "reasoning" || key === "errorMessage") && v !== null && typeof v !== "string") throw new TypeError("Expected a string or null.");
644
+ return [fields[key], v];
645
+ })));
646
+ if (bytes(encode(snapshot)) > limit) throw new TypeError("Evaluation payload exceeds its UTF-8 byte limit.");
647
+ return snapshot;
648
+ }
649
+ function restoredPayload(value, fields) {
650
+ const source = object(value);
651
+ keys(source, Object.values(fields));
652
+ return Object.fromEntries(Object.entries(fields).filter(([, wire]) => Object.hasOwn(source, wire)).map(([key, wire]) => [key, source[wire]]));
653
+ }
654
+ function score(value) {
655
+ if (typeof value === "boolean") return value;
656
+ if (typeof value === "bigint") {
657
+ const result = Number(value);
658
+ if (!Number.isFinite(result) || BigInt(result) !== value) throw new TypeError("Integer scores must be exactly representable as binary64.");
659
+ return result;
660
+ }
661
+ if (typeof value === "number") {
662
+ if (!Number.isFinite(value) || Number.isInteger(value) && !Number.isSafeInteger(value)) throw new TypeError("Use bigint for exact integer scores outside the safe number range.");
663
+ return value;
664
+ }
665
+ if (!Array.isArray(value) || !value.length) throw new TypeError("Expected a boolean, finite number, or nonempty category list.");
666
+ for (let i = 0; i < value.length; i++) if (!Object.hasOwn(value, i)) throw new TypeError("Category lists cannot be sparse.");
667
+ const categories = value.map((v) => identifier(v, 128));
668
+ if (new Set(categories).size !== categories.length) throw new TypeError("Category keys must be distinct.");
669
+ return categories.sort((a, b) => {
670
+ const aa = Array.from(a, (c) => c.codePointAt(0)), bb = Array.from(b, (c) => c.codePointAt(0));
671
+ for (let i = 0; i < Math.min(aa.length, bb.length); i++) if (aa[i] !== bb[i]) return aa[i] - bb[i];
672
+ return aa.length - bb.length;
673
+ });
674
+ }
675
+ /** One immutable execution. Reuse across scorers; construct a new item for each trial. */
676
+ var EvaluationItem = class EvaluationItem {
677
+ constructor(options = {}) {
678
+ object(options);
679
+ keys(options, [
680
+ "id",
681
+ "datasetItemVersionId",
682
+ "payload",
683
+ ...Object.keys(ITEM_IDS),
684
+ ...Object.keys(ITEM_FIELDS)
685
+ ]);
686
+ this.id = uuid(options.id ?? randomUUID());
687
+ const body = { id: this.id };
688
+ for (const [key, wire] of Object.entries(ITEM_IDS)) {
689
+ const value = options[key];
690
+ if (value !== void 0 && value !== null) body[wire] = identifier(value);
691
+ }
692
+ if (options.datasetItemVersionId != null) body.dataset_item_version_id = uuid(options.datasetItemVersionId);
693
+ const data = payload(options, ITEM_FIELDS, 1048576);
694
+ if (data !== void 0) body.payload = data;
695
+ items.set(this, encode(body));
696
+ Object.freeze(this);
697
+ }
698
+ /** Detached wire-format object. Use toJSONString() for lossless cross-language persistence. */
699
+ toJSON() {
700
+ return JSON.parse(itemJSON(this));
701
+ }
702
+ /** Exact wire JSON, including large integers. Restore with fromJSON(JSON.parse(saved)). */
703
+ toJSONString() {
704
+ return itemJSON(this);
705
+ }
706
+ static fromJSON(value) {
707
+ const data = object(value);
708
+ keys(data, [
709
+ "id",
710
+ "dataset_item_version_id",
711
+ "payload",
712
+ ...Object.values(ITEM_IDS)
713
+ ]);
714
+ const options = { id: uuid(data.id) };
715
+ for (const [key, wire] of Object.entries(ITEM_IDS)) if (Object.hasOwn(data, wire)) options[key] = data[wire];
716
+ if (Object.hasOwn(data, "dataset_item_version_id")) options.datasetItemVersionId = data.dataset_item_version_id;
717
+ if (Object.hasOwn(data, "payload")) options.payload = restoredPayload(data.payload, ITEM_FIELDS);
718
+ return new EvaluationItem(options);
719
+ }
720
+ static fromDatasetItem(item, options = {}) {
721
+ if (item.archived !== false) throw new TypeError("Archived dataset items cannot be evaluated.");
722
+ keys(options, [
723
+ "output",
724
+ "id",
725
+ "caseKey",
726
+ "trial",
727
+ "applicationTraceId"
728
+ ]);
729
+ return new EvaluationItem({
730
+ ...options,
731
+ caseKey: options.caseKey ?? item.clientItemId ?? item.id,
732
+ input: item.input,
733
+ expectedOutput: item.expectedOutput,
734
+ metadata: item.metadata,
735
+ datasetItemIdentifier: uuid(item.id),
736
+ datasetItemVersionIdentifier: uuid(item.versionId),
737
+ datasetItemVersionId: item.versionId
738
+ });
739
+ }
740
+ };
741
+ /** An evaluator outcome pinned to an exact scorer version. `ok` does not imply passing. */
742
+ var EvaluationResult = class EvaluationResult {
743
+ constructor(options) {
744
+ object(options);
745
+ keys(options, [
746
+ "item",
747
+ "scorerVersionId",
748
+ "status",
749
+ "value",
750
+ "errorCode",
751
+ "evaluatorTraceId",
752
+ "evaluatedAt",
753
+ "payload",
754
+ ...Object.keys(RESULT_FIELDS)
755
+ ]);
756
+ this.item = options.item instanceof EvaluationItem ? options.item : uuid(options.item);
757
+ this.itemId = typeof this.item === "string" ? this.item : this.item.id;
758
+ this.scorerVersionId = uuid(options.scorerVersionId);
759
+ const status = options.status ?? "ok";
760
+ if (![
761
+ "ok",
762
+ "error",
763
+ "skipped",
764
+ "not_applicable"
765
+ ].includes(status)) throw new TypeError("Unsupported result status.");
766
+ if (status !== "ok" && options.value !== void 0 && options.value !== null) throw new TypeError("Only ok results can contain a score.");
767
+ if (status !== "error" && options.errorCode != null) throw new TypeError("Only error results can contain an error code.");
768
+ const body = {
769
+ item_id: this.itemId,
770
+ scorer_version_id: this.scorerVersionId,
771
+ status
772
+ };
773
+ if (status === "ok") body.value = score(options.value);
774
+ if (options.errorCode != null) body.error_code = identifier(options.errorCode, 128);
775
+ if (options.evaluatorTraceId != null) body.evaluator_trace_id = identifier(options.evaluatorTraceId);
776
+ if (options.evaluatedAt != null) body.evaluated_at = timestamp(options.evaluatedAt);
777
+ const data = payload(options, RESULT_FIELDS, 262144);
778
+ if (data !== void 0) {
779
+ if (status !== "error" && data.error_message != null) throw new TypeError("Only error results can contain an error message.");
780
+ body.payload = data;
781
+ }
782
+ results.set(this, encode(body));
783
+ Object.freeze(this);
784
+ }
785
+ /** Detached wire-format object. Use toJSONString() for lossless cross-language persistence. */
786
+ toJSON() {
787
+ return JSON.parse(resultJSON(this));
788
+ }
789
+ /** Exact wire JSON. Save the attached item separately and restore with fromJSON(JSON.parse(saved), { item }). */
790
+ toJSONString() {
791
+ return resultJSON(this);
792
+ }
793
+ static fromJSON(value, options = {}) {
794
+ const data = object(value);
795
+ keys(data, [
796
+ "item_id",
797
+ "scorer_version_id",
798
+ "status",
799
+ "value",
800
+ "error_code",
801
+ "evaluator_trace_id",
802
+ "evaluated_at",
803
+ "payload"
804
+ ]);
805
+ const itemId = uuid(data.item_id);
806
+ if (options.item !== void 0 && options.item.id !== itemId) throw new TypeError("Restored item identity does not match.");
807
+ const valueInput = typeof data.value === "number" && Number.isInteger(data.value) && !Number.isSafeInteger(data.value) && Number.isFinite(data.value) ? BigInt(data.value) : data.value;
808
+ return new EvaluationResult(defined({
809
+ item: options.item ?? itemId,
810
+ scorerVersionId: data.scorer_version_id,
811
+ status: data.status,
812
+ value: valueInput,
813
+ errorCode: data.error_code,
814
+ evaluatorTraceId: data.evaluator_trace_id,
815
+ evaluatedAt: data.evaluated_at,
816
+ payload: Object.hasOwn(data, "payload") ? restoredPayload(data.payload, RESULT_FIELDS) : void 0
817
+ }));
818
+ }
819
+ };
820
+ function itemJSON(item) {
821
+ const value = items.get(item);
822
+ if (value === void 0) throw new TypeError("Expected an EvaluationItem.");
823
+ return value;
824
+ }
825
+ function resultJSON(result) {
826
+ const value = results.get(result);
827
+ if (value === void 0) throw new TypeError("Expected an EvaluationResult.");
828
+ return value;
829
+ }
830
+ //#endregion
831
+ //#region src/evaluations/scorers.ts
832
+ const CONFIG_KEYS = {
833
+ trueLabel: "true_label",
834
+ falseLabel: "false_label",
835
+ trueIsFailure: "true_is_failure",
836
+ passingRule: "passing_rule",
837
+ selectionMode: "selection_mode",
838
+ minSelections: "min_selections",
839
+ maxSelections: "max_selections"
840
+ };
841
+ function kind(value) {
842
+ if (value !== "boolean" && value !== "numeric" && value !== "categorical") throw new TypeError("Unsupported scorer kind.");
843
+ return value;
844
+ }
845
+ function config(value, toWire) {
846
+ const fields = toWire ? CONFIG_KEYS : Object.fromEntries(Object.entries(CONFIG_KEYS).map(([k, v]) => [v, k]));
847
+ return Object.fromEntries(Object.entries(toWire ? metadata(value) : responseMetadata(value)).map(([key, v]) => [Object.hasOwn(fields, key) ? fields[key] : key, v]));
848
+ }
849
+ function parsedConfig(value, raw) {
850
+ const selected = kind(value);
851
+ const snapshot = config(raw, false);
852
+ const finite = (v) => {
853
+ if (typeof v !== "number" || !Number.isFinite(v)) throw new TypeError("Expected a finite configuration number.");
854
+ };
855
+ if (selected === "boolean") {
856
+ for (const key of ["trueLabel", "falseLabel"]) if (snapshot[key] !== void 0) text(snapshot[key]);
857
+ if (snapshot.trueIsFailure != null) boolean(snapshot.trueIsFailure);
858
+ return {
859
+ kind: selected,
860
+ config: snapshot
861
+ };
862
+ }
863
+ if (selected === "numeric") {
864
+ for (const key of [
865
+ "min",
866
+ "max",
867
+ "step"
868
+ ]) if (snapshot[key] != null) finite(snapshot[key]);
869
+ if (snapshot.passingRule != null) {
870
+ const rule = object(snapshot.passingRule);
871
+ if (rule.operator !== "gte" && rule.operator !== "lte") throw new TypeError("Invalid passing operator.");
872
+ finite(rule.threshold);
873
+ }
874
+ return {
875
+ kind: selected,
876
+ config: snapshot
877
+ };
878
+ }
879
+ if (!Array.isArray(snapshot.options)) throw new TypeError("Expected categorical options.");
880
+ for (const option of snapshot.options) {
881
+ const row = object(option);
882
+ text(row.key);
883
+ text(row.label);
884
+ }
885
+ if (snapshot.selectionMode !== void 0 && snapshot.selectionMode !== "single" && snapshot.selectionMode !== "multiple") throw new TypeError("Invalid selection mode.");
886
+ for (const key of ["minSelections", "maxSelections"]) if (snapshot[key] != null) integer(snapshot[key]);
887
+ if (snapshot.passingRule != null) {
888
+ const categories = object(snapshot.passingRule).categories;
889
+ if (!Array.isArray(categories)) throw new TypeError("Expected passing categories.");
890
+ categories.forEach(text);
891
+ }
892
+ return {
893
+ kind: selected,
894
+ config: snapshot
895
+ };
896
+ }
897
+ function scorer(data, project, id) {
898
+ const identity = uuid(data.id);
899
+ if (id !== void 0 && identity !== id || integer(data.team) !== project) throw new TypeError("Mismatched scorer scope.");
900
+ return {
901
+ id: identity,
902
+ name: text(data.name),
903
+ description: text(data.description),
904
+ archived: boolean(data.archived),
905
+ ...parsedConfig(data.kind, data.config),
906
+ tags: responseTags(data.tags === void 0 ? [] : data.tags),
907
+ currentVersion: integer(data.current_version),
908
+ currentVersionId: uuid(data.current_version_id),
909
+ team: project,
910
+ createdAt: timestamp(data.created_at),
911
+ updatedAt: timestamp(data.updated_at),
912
+ createdBy: nullable(data.created_by, responseMetadata)
913
+ };
914
+ }
915
+ function version(data, scorerId, versionId) {
916
+ const id = uuid(data.id);
917
+ if (uuid(data.definition_id) !== scorerId || versionId !== void 0 && versionId !== id) throw new TypeError("Mismatched scorer version.");
918
+ return {
919
+ id,
920
+ definitionId: scorerId,
921
+ version: integer(data.version),
922
+ ...parsedConfig(data.kind, data.config),
923
+ createdAt: timestamp(data.created_at),
924
+ createdBy: nullable(data.created_by, responseMetadata)
925
+ };
926
+ }
927
+ function fields(options) {
928
+ return defined({
929
+ name: options.name === void 0 ? void 0 : identifier(options.name, 400),
930
+ description: options.description === void 0 ? void 0 : nullable(options.description, text),
931
+ archived: options.archived === void 0 ? void 0 : boolean(options.archived),
932
+ tags: options.tags === void 0 ? void 0 : tags(options.tags)
933
+ });
934
+ }
935
+ /** Versioned scorer definitions. Evaluator execution remains caller-owned. */
936
+ var Scorers = class {
937
+ #transport;
938
+ #base;
939
+ constructor(transport) {
940
+ this.#transport = transport;
941
+ this.#base = `/api/projects/${transport.projectId}/llm_analytics/score_definitions/`;
942
+ }
943
+ async #request(method, path, parse, options) {
944
+ return parseResponse(await this.#transport.request(method, path, options), parse);
945
+ }
946
+ async create(options, request = {}) {
947
+ keys(options, [
948
+ "name",
949
+ "kind",
950
+ "config",
951
+ "description",
952
+ "tags"
953
+ ]);
954
+ const selectedKind = kind(options.kind);
955
+ const body = encode(json({
956
+ ...fields(options),
957
+ name: identifier(options.name, 400),
958
+ kind: selectedKind,
959
+ config: config(options.config, true)
960
+ }));
961
+ return this.#request("POST", this.#base, (data) => {
962
+ const result = scorer(data, this.#transport.projectId);
963
+ if (result.kind !== selectedKind) throw new TypeError("Mismatched scorer kind.");
964
+ return result;
965
+ }, {
966
+ ...request,
967
+ body
968
+ });
969
+ }
970
+ async get(scorerId, request = {}) {
971
+ const id = uuid(scorerId);
972
+ return this.#request("GET", `${this.#base}${id}/`, (data) => scorer(data, this.#transport.projectId, id), request);
973
+ }
974
+ async list(options = {}) {
975
+ const { limit = 100, offset = 0 } = options;
976
+ if (options.orderBy !== void 0 && ![
977
+ "name",
978
+ "-name",
979
+ "kind",
980
+ "-kind",
981
+ "created_at",
982
+ "-created_at",
983
+ "updated_at",
984
+ "-updated_at",
985
+ "current_version",
986
+ "-current_version"
987
+ ].includes(options.orderBy)) throw new TypeError("Unsupported scorer order.");
988
+ const params = {
989
+ ...pageParams(limit, offset, Number.MAX_SAFE_INTEGER),
990
+ ...tagParams(options.tags, options.tagMatch),
991
+ ...defined({
992
+ search: options.search === void 0 ? void 0 : text(options.search),
993
+ kind: options.kind === void 0 ? void 0 : kind(options.kind),
994
+ archived: options.archived === void 0 ? void 0 : String(boolean(options.archived)),
995
+ order_by: options.orderBy
996
+ })
997
+ };
998
+ return this.#request("GET", this.#base, (data) => ({
999
+ ...page(data, (row) => scorer(row, this.#transport.projectId), offset),
1000
+ next: nullable(data.next, text),
1001
+ previous: nullable(data.previous, text)
1002
+ }), {
1003
+ signal: options.signal,
1004
+ params
1005
+ });
1006
+ }
1007
+ async update(scorerId, options, request = {}) {
1008
+ keys(options, [
1009
+ "name",
1010
+ "description",
1011
+ "archived",
1012
+ "tags"
1013
+ ]);
1014
+ const id = uuid(scorerId);
1015
+ return this.#request("PATCH", `${this.#base}${id}/`, (data) => scorer(data, this.#transport.projectId, id), {
1016
+ ...request,
1017
+ body: encode(json(fields(options)))
1018
+ });
1019
+ }
1020
+ async createVersion(scorerId, options, request = {}) {
1021
+ keys(options, [
1022
+ "config",
1023
+ "baseVersion",
1024
+ "name",
1025
+ "description"
1026
+ ]);
1027
+ const id = uuid(scorerId);
1028
+ const body = encode(json({
1029
+ ...fields(options),
1030
+ config: config(options.config, true),
1031
+ ...defined({ base_version: options.baseVersion === void 0 ? void 0 : integer(options.baseVersion) })
1032
+ }));
1033
+ return this.#request("POST", `${this.#base}${id}/new_version/`, (data) => scorer(data, this.#transport.projectId, id), {
1034
+ ...request,
1035
+ body
1036
+ });
1037
+ }
1038
+ async getVersion(scorerId, versionId, request = {}) {
1039
+ const id = uuid(scorerId), selected = uuid(versionId);
1040
+ return this.#request("GET", `${this.#base}${id}/versions/${selected}/`, (data) => version(data, id, selected), request);
1041
+ }
1042
+ async listVersions(scorerId, options = {}) {
1043
+ const id = uuid(scorerId), cursor = options.cursor;
1044
+ const params = {
1045
+ limit: integer(options.limit ?? 50, 1, 100),
1046
+ ...defined({ cursor: cursor === void 0 ? void 0 : text(cursor) })
1047
+ };
1048
+ return this.#request("GET", `${this.#base}${id}/versions/`, (data) => {
1049
+ if (!Array.isArray(data.results)) throw new TypeError("Expected version results.");
1050
+ const next = nullable(data.next_cursor, text);
1051
+ if (next !== null && (!next || next === cursor || !data.results.length)) throw new TypeError("Invalid cursor continuation.");
1052
+ return {
1053
+ count: count(data.count),
1054
+ results: data.results.map((row) => version(object(row), id)),
1055
+ nextCursor: next
1056
+ };
1057
+ }, {
1058
+ signal: options.signal,
1059
+ params
1060
+ });
1061
+ }
1062
+ };
1063
+ //#endregion
1064
+ //#region src/evaluations/transport.ts
1065
+ const RETRY_STATUSES = /* @__PURE__ */ new Set([
1066
+ 408,
1067
+ 429,
1068
+ 500,
1069
+ 502,
1070
+ 503,
1071
+ 504
1072
+ ]);
1073
+ const HOSTS = {
1074
+ "app.posthog.com": "us.posthog.com",
1075
+ "us.i.posthog.com": "us.posthog.com",
1076
+ "eu.i.posthog.com": "eu.posthog.com"
1077
+ };
1078
+ function normalizeHost(host) {
1079
+ const url = new URL(text(host));
1080
+ if (!["http:", "https:"].includes(url.protocol) || url.username || url.password || url.search || url.hash || host.includes("?") || host.includes("#")) throw new TypeError("Host must be an absolute HTTP(S) URL without credentials, query, or fragment.");
1081
+ if (url.protocol === "https:" && !url.port && Object.hasOwn(HOSTS, url.hostname)) url.hostname = HOSTS[url.hostname];
1082
+ return url.href.replace(/\/+$/, "");
1083
+ }
1084
+ function retryAfter(value) {
1085
+ if (!value?.trim()) return void 0;
1086
+ const number = Number(value);
1087
+ const seconds = Number.isFinite(number) ? number : (Date.parse(value) - Date.now()) / 1e3;
1088
+ return Number.isFinite(seconds) ? Math.max(0, seconds) : void 0;
1089
+ }
1090
+ function delay(ms, signal) {
1091
+ signal.throwIfAborted();
1092
+ return new Promise((resolve, reject) => {
1093
+ const onAbort = () => {
1094
+ clearTimeout(timer);
1095
+ signal.removeEventListener("abort", onAbort);
1096
+ reject(signal.reason);
1097
+ };
1098
+ const timer = setTimeout(() => {
1099
+ signal.removeEventListener("abort", onAbort);
1100
+ resolve();
1101
+ }, ms);
1102
+ signal.addEventListener("abort", onAbort, { once: true });
1103
+ });
1104
+ }
1105
+ var Transport = class {
1106
+ #host;
1107
+ #secret;
1108
+ #timeout;
1109
+ #retries;
1110
+ #closed = new AbortController();
1111
+ constructor(options) {
1112
+ this.projectId = integer(options.projectId);
1113
+ this.#secret = text(options.secretKey);
1114
+ if (!this.#secret || /\s/.test(this.#secret)) throw new TypeError("Secret key must be nonempty and contain no whitespace.");
1115
+ this.#host = normalizeHost(options.host ?? "https://us.posthog.com");
1116
+ this.#timeout = options.timeoutMs === void 0 ? 15e3 : options.timeoutMs;
1117
+ if (typeof this.#timeout !== "number" || !Number.isFinite(this.#timeout) || this.#timeout <= 0 || this.#timeout > 2147483647) throw new TypeError("Invalid timeoutMs.");
1118
+ this.#retries = integer(options.maxRetries === void 0 ? 3 : options.maxRetries, 0);
1119
+ }
1120
+ close() {
1121
+ this.#closed.abort(new EvaluationAPIError({ code: "client_closed" }));
1122
+ }
1123
+ async request(method, path, request = {}) {
1124
+ const options = {
1125
+ ...request,
1126
+ params: request.params === void 0 ? void 0 : { ...request.params }
1127
+ };
1128
+ if (this.#closed.signal.aborted) throw new EvaluationAPIError({
1129
+ code: "client_closed",
1130
+ persistence: "not_sent"
1131
+ });
1132
+ options.signal?.throwIfAborted();
1133
+ if (!path.startsWith("/") || path.startsWith("//") || /[?#]/.test(path)) throw new TypeError("Invalid API path.");
1134
+ const url = new URL(this.#host + path);
1135
+ for (const [key, value] of Object.entries(options.params ?? {})) url.searchParams.set(key, String(value));
1136
+ const operation = new AbortController();
1137
+ const forward = (signal) => operation.abort(signal.reason);
1138
+ const externalAbort = () => forward(options.signal);
1139
+ const closeAbort = () => forward(this.#closed.signal);
1140
+ options.signal?.addEventListener("abort", externalAbort, { once: true });
1141
+ this.#closed.signal.addEventListener("abort", closeAbort, { once: true });
1142
+ let hadUnknown = false;
1143
+ try {
1144
+ for (let attempt = 0;; attempt++) {
1145
+ operation.signal.throwIfAborted();
1146
+ const controller = new AbortController();
1147
+ const abort = () => controller.abort(operation.signal.reason);
1148
+ operation.signal.addEventListener("abort", abort, { once: true });
1149
+ const timer = setTimeout(() => controller.abort(), this.#timeout);
1150
+ let error;
1151
+ try {
1152
+ const response = await fetch(url, {
1153
+ method,
1154
+ body: options.body,
1155
+ redirect: "manual",
1156
+ signal: controller.signal,
1157
+ headers: {
1158
+ Authorization: `Bearer ${this.#secret}`,
1159
+ Accept: "application/json",
1160
+ "Content-Type": "application/json"
1161
+ }
1162
+ });
1163
+ const raw = await response.text();
1164
+ operation.signal.throwIfAborted();
1165
+ let payload;
1166
+ try {
1167
+ payload = decode(raw);
1168
+ } catch {
1169
+ payload = void 0;
1170
+ }
1171
+ if (response.ok) try {
1172
+ return object(payload);
1173
+ } catch {
1174
+ throw new EvaluationAPIError({
1175
+ status: response.status,
1176
+ code: "invalid_response"
1177
+ });
1178
+ }
1179
+ let fields = {};
1180
+ try {
1181
+ fields = object(payload);
1182
+ } catch {}
1183
+ throw new EvaluationAPIError({
1184
+ status: response.status,
1185
+ code: typeof fields.code === "string" ? fields.code : void 0,
1186
+ detail: fields.detail ?? payload,
1187
+ attr: typeof fields.attr === "string" ? fields.attr : void 0,
1188
+ errors: fields.errors,
1189
+ response: Object.keys(fields).length ? fields : void 0,
1190
+ retryAfter: retryAfter(response.headers.get("Retry-After")),
1191
+ persistence: response.status >= 400 && response.status < 500 && response.status !== 408 ? "rejected" : "unknown"
1192
+ });
1193
+ } catch (caught) {
1194
+ operation.signal.throwIfAborted();
1195
+ error = caught instanceof EvaluationAPIError ? caught : new EvaluationAPIError({ code: "transport_error" });
1196
+ } finally {
1197
+ clearTimeout(timer);
1198
+ operation.signal.removeEventListener("abort", abort);
1199
+ }
1200
+ hadUnknown ||= error.persistence === "unknown";
1201
+ if (hadUnknown) error.persistence = "unknown";
1202
+ if (!(options.retrySafe ?? method === "GET") || attempt >= this.#retries || error.code === "invalid_response" || error.status !== void 0 && !RETRY_STATUSES.has(error.status) || (error.retryAfter ?? 0) > 30) throw error;
1203
+ const backoff = Math.min(30, .5 * 2 ** Math.min(attempt, 6) * (.8 + Math.random() * .4));
1204
+ await delay(Math.max(backoff, error.retryAfter ?? 0) * 1e3, operation.signal);
1205
+ }
1206
+ } finally {
1207
+ options.signal?.removeEventListener("abort", externalAbort);
1208
+ this.#closed.signal.removeEventListener("abort", closeAbort);
1209
+ }
1210
+ }
1211
+ };
1212
+ /** One handle's operations are atomic with respect to each other, including all bulk chunks. */
1213
+ var OperationQueue = class {
1214
+ #tail = Promise.resolve();
1215
+ async run(operation, signal) {
1216
+ signal?.throwIfAborted();
1217
+ const previous = this.#tail;
1218
+ let release;
1219
+ const done = new Promise((resolve) => {
1220
+ release = resolve;
1221
+ });
1222
+ this.#tail = previous.then(() => done);
1223
+ let abort;
1224
+ try {
1225
+ if (signal) {
1226
+ await Promise.race([previous, new Promise((_, reject) => {
1227
+ abort = () => reject(signal.reason);
1228
+ signal.addEventListener("abort", abort, { once: true });
1229
+ })]);
1230
+ signal.throwIfAborted();
1231
+ } else await previous;
1232
+ return await operation();
1233
+ } finally {
1234
+ if (abort) signal.removeEventListener("abort", abort);
1235
+ release();
1236
+ }
1237
+ }
1238
+ };
1239
+ //#endregion
1240
+ //#region src/evaluations/client.ts
1241
+ const EXPERIMENT_TEXT = {
1242
+ suiteKey: "suite_key",
1243
+ datasetSource: "dataset_source",
1244
+ datasetIdentifier: "dataset_identifier",
1245
+ datasetRevisionIdentifier: "dataset_revision_identifier",
1246
+ applicationVersion: "application_version",
1247
+ modelVersion: "model_version",
1248
+ promptVersion: "prompt_version"
1249
+ };
1250
+ function experimentBody(options) {
1251
+ keys(options, [
1252
+ "name",
1253
+ "id",
1254
+ "startedAt",
1255
+ "runSource",
1256
+ "expectedItemCount",
1257
+ "expectedResultCount",
1258
+ "datasetRevisionId",
1259
+ ...Object.keys(EXPERIMENT_TEXT)
1260
+ ]);
1261
+ const body = {
1262
+ name: identifier(options.name, 400),
1263
+ id: uuid(options.id === void 0 ? randomUUID() : options.id),
1264
+ started_at: timestamp(options.startedAt === void 0 ? /* @__PURE__ */ new Date() : options.startedAt)
1265
+ };
1266
+ for (const [key, wire] of Object.entries(EXPERIMENT_TEXT)) {
1267
+ const value = options[key];
1268
+ if (value !== void 0) body[wire] = nullable(value, identifier);
1269
+ }
1270
+ if (options.runSource !== void 0) {
1271
+ if (options.runSource !== null && ![
1272
+ "ci",
1273
+ "local",
1274
+ "scheduled"
1275
+ ].includes(options.runSource)) throw new TypeError("Invalid run source.");
1276
+ body.run_source = options.runSource;
1277
+ }
1278
+ if (options.expectedItemCount !== void 0) body.expected_item_count = nullable(options.expectedItemCount, count);
1279
+ if (options.expectedResultCount !== void 0) body.expected_result_count = nullable(options.expectedResultCount, count);
1280
+ if (options.datasetRevisionId !== void 0) body.dataset_revision_id = nullable(options.datasetRevisionId, uuid);
1281
+ return encode(body);
1282
+ }
1283
+ function experimentReceipt(data, id) {
1284
+ if (uuid(data.id) !== id || typeof data.status !== "string" || ![
1285
+ "uploading",
1286
+ "completed",
1287
+ "failed"
1288
+ ].includes(data.status)) throw new TypeError("Mismatched experiment receipt.");
1289
+ return {
1290
+ id,
1291
+ status: data.status,
1292
+ created: boolean(data.created),
1293
+ startedAt: timestamp(data.started_at),
1294
+ createdAt: timestamp(data.created_at),
1295
+ finishedAt: nullable(data.finished_at, timestamp),
1296
+ expectedItemCount: nullable(data.expected_item_count, count),
1297
+ expectedResultCount: nullable(data.expected_result_count, count),
1298
+ acceptedItemCount: count(data.accepted_item_count),
1299
+ acceptedResultCount: count(data.accepted_result_count)
1300
+ };
1301
+ }
1302
+ function itemReceipt(value) {
1303
+ const data = object(value);
1304
+ return {
1305
+ id: uuid(data.id),
1306
+ created: boolean(data.created),
1307
+ acceptedAt: timestamp(data.accepted_at)
1308
+ };
1309
+ }
1310
+ function uploadReceipt(data, submitted) {
1311
+ if (!Array.isArray(data.items) || !Array.isArray(data.results)) throw new TypeError("Expected acknowledgment lists.");
1312
+ const items = data.items.map(itemReceipt);
1313
+ const results = data.results.map((value) => {
1314
+ const row = object(value);
1315
+ return {
1316
+ ...itemReceipt(row),
1317
+ itemId: uuid(row.item_id),
1318
+ scorerVersionId: uuid(row.scorer_version_id)
1319
+ };
1320
+ });
1321
+ const expectedItems = new Set(submitted.map((r) => r.itemId)), actualItems = new Set(items.map((i) => i.id));
1322
+ if (actualItems.size !== items.length || actualItems.size !== expectedItems.size || [...expectedItems].some((id) => !actualItems.has(id))) throw new TypeError("Incomplete item acknowledgments.");
1323
+ if (results.length !== submitted.length || results.some((r, i) => r.itemId !== submitted[i].itemId || r.scorerVersionId !== submitted[i].scorerVersionId) || new Set(results.map((r) => r.id)).size !== results.length) throw new TypeError("Incomplete result acknowledgments.");
1324
+ return {
1325
+ items,
1326
+ results
1327
+ };
1328
+ }
1329
+ function uploadBody(items, results) {
1330
+ return `{"items":[${items.join(",")}],"results":[${results.join(",")}]}`;
1331
+ }
1332
+ function chunks(input) {
1333
+ if (!Array.isArray(input)) throw new TypeError("uploadResults expects a finite array of EvaluationResult declarations.");
1334
+ const original = Array.from(input), identities = /* @__PURE__ */ new Set(), declarations = /* @__PURE__ */ new Map();
1335
+ for (const result of original) {
1336
+ resultJSON(result);
1337
+ const pair = `${result.itemId}/${result.scorerVersionId}`;
1338
+ if (identities.has(pair)) throw new TypeError("Duplicate item/scorer-version pair.");
1339
+ identities.add(pair);
1340
+ if (result.item instanceof EvaluationItem) {
1341
+ const previous = declarations.get(result.itemId);
1342
+ if (previous && itemJSON(previous) !== itemJSON(result.item)) throw new TypeError("Conflicting item declarations.");
1343
+ declarations.set(result.itemId, result.item);
1344
+ }
1345
+ }
1346
+ const prepared = original.map((r) => typeof r.item === "string" && declarations.has(r.itemId) ? EvaluationResult.fromJSON(r.toJSON(), { item: declarations.get(r.itemId) }) : r);
1347
+ const output = [];
1348
+ let start = 0, size = bytes(uploadBody([], [])), encodedItems = [], encodedResults = [], itemIds = /* @__PURE__ */ new Set();
1349
+ for (let index = 0; index < prepared.length; index++) {
1350
+ const result = prepared[index], resultBody = resultJSON(result), declaration = declarations.get(result.itemId);
1351
+ let itemBody = declaration && !itemIds.has(result.itemId) ? itemJSON(declaration) : void 0;
1352
+ let extra = bytes(resultBody) + (encodedResults.length ? 1 : 0) + (itemBody === void 0 ? 0 : bytes(itemBody) + (encodedItems.length ? 1 : 0));
1353
+ if (encodedResults.length && (encodedResults.length >= 1e3 || encodedItems.length + (itemBody === void 0 ? 0 : 1) > 1e3 || size + extra > 5242880)) {
1354
+ output.push({
1355
+ start,
1356
+ results: prepared.slice(start, index),
1357
+ body: uploadBody(encodedItems, encodedResults)
1358
+ });
1359
+ start = index;
1360
+ size = bytes(uploadBody([], []));
1361
+ encodedItems = [];
1362
+ encodedResults = [];
1363
+ itemIds = /* @__PURE__ */ new Set();
1364
+ itemBody = declaration ? itemJSON(declaration) : void 0;
1365
+ extra = bytes(resultBody) + (itemBody === void 0 ? 0 : bytes(itemBody));
1366
+ }
1367
+ if (size + extra > 5242880) throw new TypeError("One item/result pair exceeds the request byte limit.");
1368
+ if (itemBody !== void 0) {
1369
+ itemIds.add(result.itemId);
1370
+ encodedItems.push(itemBody);
1371
+ }
1372
+ encodedResults.push(resultBody);
1373
+ size += extra;
1374
+ }
1375
+ if (encodedResults.length) output.push({
1376
+ start,
1377
+ results: prepared.slice(start),
1378
+ body: uploadBody(encodedItems, encodedResults)
1379
+ });
1380
+ return {
1381
+ chunks: output,
1382
+ prepared
1383
+ };
1384
+ }
1385
+ /** Earlier chunks are acknowledged; pendingResults includes the uncertain failed chunk. */
1386
+ var BulkUploadError = class extends EvaluationAPIError {
1387
+ #completed;
1388
+ #pending;
1389
+ #failed;
1390
+ constructor(cause, completed, failed, pending) {
1391
+ super(cause);
1392
+ this.name = "BulkUploadError";
1393
+ this.failedIndex = failed.start;
1394
+ this.#completed = copy(completed);
1395
+ this.#pending = pending.slice();
1396
+ this.#failed = failed.body;
1397
+ }
1398
+ get completed() {
1399
+ return copy(this.#completed);
1400
+ }
1401
+ get pendingResults() {
1402
+ return this.#pending.slice();
1403
+ }
1404
+ get failedChunk() {
1405
+ return JSON.parse(this.#failed);
1406
+ }
1407
+ };
1408
+ var Experiment = class {
1409
+ #transport;
1410
+ #base;
1411
+ #queue = new OperationQueue();
1412
+ #submission;
1413
+ #receipt;
1414
+ constructor(transport, id, receipt, submission) {
1415
+ this.id = uuid(id);
1416
+ this.#transport = transport;
1417
+ this.#receipt = receipt;
1418
+ this.#submission = submission;
1419
+ this.#base = `/api/projects/${transport.projectId}/ai_observability/offline_experiments/${this.id}/`;
1420
+ Object.freeze(this);
1421
+ }
1422
+ get receipt() {
1423
+ return this.#receipt === void 0 ? void 0 : copy(this.#receipt);
1424
+ }
1425
+ /** Original creation options, suitable for createExperiment(submission). Large counts may be bigint. */
1426
+ get submission() {
1427
+ return this.#submission === void 0 ? void 0 : submissionOptions(object(decode(this.#submission)));
1428
+ }
1429
+ async uploadResult(options, request = {}) {
1430
+ const chunk = chunks([new EvaluationResult(options)]).chunks[0];
1431
+ return this.#queue.run(() => this.#upload(chunk, request), request.signal);
1432
+ }
1433
+ async #upload(chunk, request) {
1434
+ return parseResponse(await this.#transport.request("POST", `${this.#base}upload/`, {
1435
+ ...request,
1436
+ body: chunk.body,
1437
+ retrySafe: true
1438
+ }), (data) => uploadReceipt(data, chunk.results));
1439
+ }
1440
+ async uploadResults(results, request = {}) {
1441
+ const prepared = chunks(results);
1442
+ return this.#queue.run(async () => {
1443
+ const receipts = [];
1444
+ for (const chunk of prepared.chunks) try {
1445
+ receipts.push(await this.#upload(chunk, request));
1446
+ } catch (error) {
1447
+ if (!(error instanceof EvaluationAPIError)) throw error;
1448
+ throw new BulkUploadError(error, { chunks: receipts }, chunk, prepared.prepared.slice(chunk.start));
1449
+ }
1450
+ return { chunks: receipts };
1451
+ }, request.signal);
1452
+ }
1453
+ async complete(request = {}) {
1454
+ return this.#finish("complete", request);
1455
+ }
1456
+ async fail(request = {}) {
1457
+ return this.#finish("fail", request);
1458
+ }
1459
+ async #finish(action, request) {
1460
+ return this.#queue.run(async () => {
1461
+ const receipt = parseResponse(await this.#transport.request("POST", `${this.#base}${action}/`, {
1462
+ ...request,
1463
+ body: "{}",
1464
+ retrySafe: true
1465
+ }), (data) => {
1466
+ const result = experimentReceipt(data, this.id);
1467
+ if (result.status !== (action === "complete" ? "completed" : "failed")) throw new TypeError("Unexpected experiment status.");
1468
+ return result;
1469
+ });
1470
+ this.#receipt = copy(receipt);
1471
+ return receipt;
1472
+ }, request.signal);
1473
+ }
1474
+ async getTags(request = {}) {
1475
+ return this.#tags("GET", void 0, request);
1476
+ }
1477
+ async setTags(names, request = {}) {
1478
+ return this.#tags("PATCH", encode({ tags: tags(names) }), request);
1479
+ }
1480
+ async #tags(method, body, request) {
1481
+ return this.#queue.run(async () => parseResponse(await this.#transport.request(method, this.#base, {
1482
+ ...request,
1483
+ body
1484
+ }), (data) => {
1485
+ if (uuid(data.id) !== this.id) throw new TypeError("Mismatched experiment.");
1486
+ return responseTags(data.tags);
1487
+ }), request.signal);
1488
+ }
1489
+ };
1490
+ function submissionOptions(data) {
1491
+ const result = {
1492
+ name: data.name,
1493
+ id: data.id,
1494
+ startedAt: data.started_at
1495
+ };
1496
+ for (const [key, wire] of Object.entries({
1497
+ ...EXPERIMENT_TEXT,
1498
+ runSource: "run_source",
1499
+ expectedItemCount: "expected_item_count",
1500
+ expectedResultCount: "expected_result_count",
1501
+ datasetRevisionId: "dataset_revision_id"
1502
+ })) if (Object.hasOwn(data, wire)) result[key] = data[wire];
1503
+ return result;
1504
+ }
1505
+ /** Acknowledged management APIs, independent of analytics capture and flush. */
1506
+ var OfflineEvaluations = class {
1507
+ #transport;
1508
+ constructor(options) {
1509
+ this.#transport = new Transport(options);
1510
+ this.datasets = new Datasets(this.#transport);
1511
+ this.scorers = new Scorers(this.#transport);
1512
+ Object.freeze(this);
1513
+ }
1514
+ async createExperiment(options, request = {}) {
1515
+ const signal = request.signal, body = experimentBody(options), submission = object(decode(body)), id = uuid(submission.id);
1516
+ try {
1517
+ const data = await this.#transport.request("POST", `/api/projects/${this.#transport.projectId}/ai_observability/offline_experiments/`, {
1518
+ ...request,
1519
+ body,
1520
+ retrySafe: true
1521
+ });
1522
+ return new Experiment(this.#transport, id, parseResponse(data, (value) => experimentReceipt(value, id)), body);
1523
+ } catch (error) {
1524
+ if (signal?.aborted && Object.is(error, signal.reason)) throw new ExperimentCreationAbortedError(error, submissionOptions(submission));
1525
+ if (!(error instanceof EvaluationAPIError)) throw error;
1526
+ throw new EvaluationAPIError({
1527
+ ...error,
1528
+ detail: error.detail,
1529
+ errors: error.errors,
1530
+ response: error.response,
1531
+ submission: submissionOptions(submission)
1532
+ });
1533
+ }
1534
+ }
1535
+ /** Local only; existence and permissions are checked on the next network operation. */
1536
+ resumeExperiment(id) {
1537
+ return new Experiment(this.#transport, id);
1538
+ }
1539
+ /** Stop owned requests and timers. This never completes or fails experiments. */
1540
+ close() {
1541
+ this.#transport.close();
1542
+ }
1543
+ };
1544
+ //#endregion
1545
+ export { BulkUploadError, DatasetSnapshot, Datasets, EvaluationAPIError, EvaluationItem, EvaluationResult, Experiment, ExperimentCreationAbortedError, OfflineEvaluations, Scorers };
1546
+
1547
+ //# sourceMappingURL=index.mjs.map