@hue-run/sdk 0.17.0 → 0.18.1
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/CLI.md +130 -8
- package/EVALUATIONS.md +56 -2
- package/README.md +299 -37
- package/dist/attachment.d.ts +82 -0
- package/dist/attachment.js +225 -0
- package/dist/blobs.d.ts +41 -3
- package/dist/blobs.js +126 -41
- package/dist/cli/import.d.ts +35 -0
- package/dist/cli/import.js +692 -0
- package/dist/cli/langfuse.d.ts +309 -0
- package/dist/cli/langfuse.js +1075 -0
- package/dist/client.d.ts +18 -0
- package/dist/client.js +148 -13
- package/dist/evals/client.d.ts +19 -1
- package/dist/evals/client.js +91 -11
- package/dist/evals/files.d.ts +11 -1
- package/dist/evals/files.js +248 -20
- package/dist/evals/media.d.ts +221 -0
- package/dist/evals/media.js +919 -0
- package/dist/evals/runner.js +130 -106
- package/dist/evals/types.d.ts +49 -0
- package/dist/evals.d.ts +2 -0
- package/dist/evals.js +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/inline-files.d.ts +140 -8
- package/dist/inline-files.js +572 -51
- package/dist/json-edits.d.ts +24 -0
- package/dist/json-edits.js +187 -0
- package/dist/privacy.d.ts +13 -0
- package/dist/privacy.js +84 -17
- package/dist/safety.d.ts +6 -2
- package/dist/safety.js +15 -3
- package/dist/setup/cli.js +7 -3
- package/dist/snapshot.d.ts +1 -1
- package/dist/snapshot.js +123 -29
- package/dist/transport.d.ts +10 -0
- package/dist/transport.js +296 -56
- package/dist/types.d.ts +25 -6
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/evals/files.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { constants } from "node:fs";
|
|
3
|
-
import { lstat, mkdir, open, readFile, rename, rm } from "node:fs/promises";
|
|
3
|
+
import { lstat, mkdir, open, readFile, rename, rm, stat } from "node:fs/promises";
|
|
4
4
|
import { extname, join, resolve } from "node:path";
|
|
5
5
|
import { randomUUID } from "node:crypto";
|
|
6
6
|
import { ArtifactSizeError, HueApiError } from "./client.js";
|
|
7
|
+
import { uuid } from "./json.js";
|
|
7
8
|
/** Roles the target receives. Organization templates stay with grading, as in the managed protocol. */
|
|
8
9
|
export const targetFileRoles = [
|
|
9
10
|
"source",
|
|
@@ -413,33 +414,260 @@ export async function uploadOutputFiles(client, executionId, files, save, timing
|
|
|
413
414
|
if (file.artifactId)
|
|
414
415
|
continue;
|
|
415
416
|
const bytes = await verifiedBytes(file.path, file);
|
|
416
|
-
|
|
417
|
+
file.artifactId = await publishArtifact(client, {
|
|
417
418
|
idempotencyKey: `hue-sdk:${executionId}:${file.sha256}:${sha256(Buffer.from(file.filename)).slice(0, 16)}`,
|
|
418
419
|
filename: file.filename,
|
|
419
420
|
contentType: file.contentType,
|
|
420
421
|
byteSize: file.byteSize,
|
|
421
422
|
sha256: file.sha256,
|
|
422
|
-
});
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
423
|
+
}, bytes, `Generated file ${file.filename}`, timing);
|
|
424
|
+
await save();
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
/** Reserve, stage and verify one artifact; its ID once Hue verified it. Replaying the key settles
|
|
428
|
+
* on the same artifact, including one Hue finished verifying after an earlier attempt stopped. */
|
|
429
|
+
async function publishArtifact(client, declared, body, label, timing) {
|
|
430
|
+
const reserved = await client.reserveArtifact(declared);
|
|
431
|
+
let state = reserved.state;
|
|
432
|
+
if (state !== "ready") {
|
|
433
|
+
// A verification already running holds the uploaded bytes; Hue refuses another upload then.
|
|
434
|
+
if (reserved.copyState !== "acknowledged" && state !== "verifying") {
|
|
435
|
+
// A checked file is read again, whole, only now and only if it still has the bytes its
|
|
436
|
+
// check hashed: a file that changed sends nothing.
|
|
437
|
+
const bytes = body instanceof Uint8Array ? body : await checkedBytes(body);
|
|
438
|
+
const upload = await client.requestArtifactUpload(reserved.id);
|
|
439
|
+
try {
|
|
440
|
+
if (body instanceof Uint8Array)
|
|
441
|
+
await client.uploadArtifactBytes(upload, bytes, declared.contentType);
|
|
442
|
+
// Sent from the write's own buffer a slice at a time, never copied whole.
|
|
443
|
+
else
|
|
444
|
+
await client.uploadArtifactStream(upload, slicesOf(bytes), bytes.byteLength, declared.contentType);
|
|
445
|
+
}
|
|
446
|
+
catch {
|
|
447
|
+
// A lost staging acknowledgement or an immutable object already present can only be
|
|
448
|
+
// settled by verified completion; never rewrite a final object here.
|
|
435
449
|
}
|
|
436
|
-
state = await settleArtifact(client, reserved.id, timing);
|
|
437
450
|
}
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
451
|
+
state = await settleArtifact(client, reserved.id, timing);
|
|
452
|
+
}
|
|
453
|
+
if (state !== "ready")
|
|
454
|
+
throw new Error(`${label} was not verified by Hue (${state})`);
|
|
455
|
+
return reserved.id;
|
|
456
|
+
}
|
|
457
|
+
/** How much of a case file given by path its check reads at once, to hash it. */
|
|
458
|
+
const CASE_FILE_SLICE = 1024 * 1024;
|
|
459
|
+
/** Bytes as a stream of 1 MiB views of them, made as the request reads them. */
|
|
460
|
+
function slicesOf(bytes) {
|
|
461
|
+
let offset = 0;
|
|
462
|
+
return new ReadableStream({
|
|
463
|
+
pull(controller) {
|
|
464
|
+
if (offset >= bytes.byteLength)
|
|
465
|
+
return controller.close();
|
|
466
|
+
const end = Math.min(bytes.byteLength, offset + CASE_FILE_SLICE);
|
|
467
|
+
controller.enqueue(bytes.subarray(offset, end));
|
|
468
|
+
offset = end;
|
|
469
|
+
},
|
|
470
|
+
}, { highWaterMark: 0 });
|
|
471
|
+
}
|
|
472
|
+
/**
|
|
473
|
+
* The bytes of a checked case file, read whole when it is uploaded: case files are uploaded one
|
|
474
|
+
* at a time, into one buffer the write reuses, so no more than one is held in memory. Only bytes
|
|
475
|
+
* with the checked size and SHA-256 are returned, so the bytes sent are the bytes the check
|
|
476
|
+
* hashed; a file that changed after its check, or can no longer be read, is refused with a
|
|
477
|
+
* `TypeError` before anything is sent. The bytes are valid until the next file is read.
|
|
478
|
+
*/
|
|
479
|
+
async function checkedBytes(file) {
|
|
480
|
+
const changed = () => new TypeError(`${file.label} changed after it was checked`);
|
|
481
|
+
const unreadable = (error) => new TypeError(`${file.label} could not be read`, { cause: error });
|
|
482
|
+
const handle = await open(file.path, "r").catch((error) => {
|
|
483
|
+
throw unreadable(error);
|
|
484
|
+
});
|
|
485
|
+
try {
|
|
486
|
+
const read = (buffer, offset) => handle
|
|
487
|
+
.read(buffer, offset, buffer.byteLength - offset, null)
|
|
488
|
+
.then(({ bytesRead }) => bytesRead)
|
|
489
|
+
.catch((error) => {
|
|
490
|
+
throw unreadable(error);
|
|
491
|
+
});
|
|
492
|
+
// Never more than its checked size and one byte, the byte that shows it grew.
|
|
493
|
+
if ((file.scratch.buffer?.byteLength ?? 0) < file.byteSize)
|
|
494
|
+
file.scratch.buffer = Buffer.allocUnsafe(file.byteSize);
|
|
495
|
+
const bytes = file.scratch.buffer.subarray(0, file.byteSize);
|
|
496
|
+
for (let offset = 0; offset < bytes.byteLength;) {
|
|
497
|
+
const bytesRead = await read(bytes, offset);
|
|
498
|
+
if (!bytesRead)
|
|
499
|
+
throw changed();
|
|
500
|
+
offset += bytesRead;
|
|
501
|
+
}
|
|
502
|
+
if ((await read(Buffer.alloc(1), 0)) || sha256(bytes) !== file.sha256)
|
|
503
|
+
throw changed();
|
|
504
|
+
return bytes;
|
|
505
|
+
}
|
|
506
|
+
finally {
|
|
507
|
+
await handle.close().catch(() => undefined);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
/** Roles a pinned case input can carry. */
|
|
511
|
+
const caseFileRoles = [
|
|
512
|
+
...targetFileRoles,
|
|
513
|
+
"org_template",
|
|
514
|
+
"evaluator_reference",
|
|
515
|
+
];
|
|
516
|
+
/** The most input files one case pins. */
|
|
517
|
+
const MAX_CASE_FILES = 32;
|
|
518
|
+
/** A case file name as the artifact API takes it: well-formed Unicode of 1–255 UTF-16 code units,
|
|
519
|
+
* as Hue counts them, without a control character or a path separator. */
|
|
520
|
+
function caseFileName(name) {
|
|
521
|
+
if (typeof name !== "string" ||
|
|
522
|
+
!name.length ||
|
|
523
|
+
name.length > 255 ||
|
|
524
|
+
!name.isWellFormed() ||
|
|
525
|
+
[...name].some((c) => c.charCodeAt(0) < 32 || c.charCodeAt(0) === 127 || c === "/" || c === "\\"))
|
|
526
|
+
throw new TypeError("A case file needs a well-formed filename of 1–255 UTF-16 code units without path separators or control characters");
|
|
527
|
+
return name;
|
|
528
|
+
}
|
|
529
|
+
/** A case file given by path, checked: a regular file within the size limit, hashed a slice at a
|
|
530
|
+
* time so it is never held whole. Its emptiness is the caller's check, as for bytes. */
|
|
531
|
+
async function checkedFile(path, label, scratch) {
|
|
532
|
+
if (typeof path !== "string" || !path)
|
|
533
|
+
throw new TypeError(`${label} needs bytes or a path`);
|
|
534
|
+
// A path with no file behind it, or one that cannot be read, is refused as any invalid file.
|
|
535
|
+
const unreadable = (error) => new TypeError(`${label} could not be read`, { cause: error });
|
|
536
|
+
const info = await stat(path).catch((error) => {
|
|
537
|
+
throw unreadable(error);
|
|
538
|
+
});
|
|
539
|
+
if (!info.isFile())
|
|
540
|
+
throw new TypeError(`${label} is not a regular file`);
|
|
541
|
+
if (info.size > outputFileLimits.bytes)
|
|
542
|
+
throw new RangeError(`${label} exceeds 25 MiB`);
|
|
543
|
+
const handle = await open(path, "r").catch((error) => {
|
|
544
|
+
throw unreadable(error);
|
|
545
|
+
});
|
|
546
|
+
try {
|
|
547
|
+
const hash = createHash("sha256");
|
|
548
|
+
const slice = Buffer.allocUnsafe(CASE_FILE_SLICE);
|
|
549
|
+
let byteSize = 0;
|
|
550
|
+
for (;;) {
|
|
551
|
+
const { bytesRead } = await handle
|
|
552
|
+
.read(slice, 0, slice.byteLength, null)
|
|
553
|
+
.catch((error) => {
|
|
554
|
+
throw unreadable(error);
|
|
555
|
+
});
|
|
556
|
+
if (!bytesRead)
|
|
557
|
+
break;
|
|
558
|
+
byteSize += bytesRead;
|
|
559
|
+
// A file that grew past the limit since its size was read is refused, not read whole.
|
|
560
|
+
if (byteSize > outputFileLimits.bytes)
|
|
561
|
+
throw new RangeError(`${label} exceeds 25 MiB`);
|
|
562
|
+
hash.update(slice.subarray(0, bytesRead));
|
|
563
|
+
}
|
|
564
|
+
return { path, label, byteSize, sha256: hash.digest("hex"), scratch };
|
|
565
|
+
}
|
|
566
|
+
finally {
|
|
567
|
+
await handle.close().catch(() => undefined);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
/** A stand-in for an artifact a case file is uploaded as, the length of a real ID. */
|
|
571
|
+
const STAND_IN_ID = "00000000-0000-4000-8000-000000000000";
|
|
572
|
+
/**
|
|
573
|
+
* The `files` of a case write as the case API takes them: references kept, and each file given
|
|
574
|
+
* as bytes or a path checked against the artifact policy (role, name, one of
|
|
575
|
+
* `outputContentTypes`, 1 byte to 25 MiB) and uploaded as a verified artifact. Every file is
|
|
576
|
+
* checked, and then `beforeUpload` is given the references with a stand-in ID for each upload,
|
|
577
|
+
* before the first upload. The reservation key names the version, the case's `externalKey`, the
|
|
578
|
+
* role and the bytes, so the same write sent again reuses its artifacts. Each artifact is pinned
|
|
579
|
+
* once with a role, even when a reference names the artifact a file was uploaded as.
|
|
580
|
+
*/
|
|
581
|
+
export async function caseFileReferences(client, versionId, input, beforeUpload = () => undefined, timing = artifactSettling) {
|
|
582
|
+
const files = input.files;
|
|
583
|
+
if (!Array.isArray(files))
|
|
584
|
+
throw new TypeError("files must be a list of case files");
|
|
585
|
+
if (files.length > MAX_CASE_FILES)
|
|
586
|
+
throw new RangeError(`A case pins at most ${MAX_CASE_FILES} files`);
|
|
587
|
+
const prepared = [];
|
|
588
|
+
const seen = new Set();
|
|
589
|
+
const scratch = {};
|
|
590
|
+
for (const [index, file] of files.entries()) {
|
|
591
|
+
const label = `Case file ${index + 1}`;
|
|
592
|
+
if (!file || typeof file !== "object")
|
|
593
|
+
throw new TypeError(`${label} is not a file`);
|
|
594
|
+
if (!caseFileRoles.includes(file.role))
|
|
595
|
+
throw new TypeError(`${label} has an unsupported role ${String(file.role)}`);
|
|
596
|
+
let identity;
|
|
597
|
+
if (file.artifactId !== undefined) {
|
|
598
|
+
// A reference keeps the artifact's own name and type; a file field beside it is refused
|
|
599
|
+
// rather than ignored.
|
|
600
|
+
const fields = file;
|
|
601
|
+
if (fields.filename !== undefined ||
|
|
602
|
+
fields.contentType !== undefined ||
|
|
603
|
+
fields.bytes !== undefined ||
|
|
604
|
+
fields.path !== undefined)
|
|
605
|
+
throw new TypeError(`${label} names an artifactId and a file; give one`);
|
|
606
|
+
const reference = { artifactId: uuid(file.artifactId).toLowerCase(), role: file.role };
|
|
607
|
+
prepared.push(reference);
|
|
608
|
+
identity = `artifact:${reference.artifactId}`;
|
|
609
|
+
}
|
|
610
|
+
else {
|
|
611
|
+
const filename = caseFileName(file.filename);
|
|
612
|
+
if (!outputContentTypes.includes(file.contentType))
|
|
613
|
+
throw new TypeError(`${label} has an unsupported content type ${String(file.contentType)}`);
|
|
614
|
+
if (file.bytes !== undefined && file.path !== undefined)
|
|
615
|
+
throw new TypeError(`${label} names bytes and a path; give one`);
|
|
616
|
+
if (file.bytes !== undefined && !(file.bytes instanceof Uint8Array))
|
|
617
|
+
throw new TypeError(`${label} bytes must be a Uint8Array`);
|
|
618
|
+
// Bytes are kept as given; a path is kept as its size and digest, and read again to upload.
|
|
619
|
+
const body = file.bytes ?? (await checkedFile(file.path, label, scratch));
|
|
620
|
+
const byteSize = body instanceof Uint8Array ? body.byteLength : body.byteSize;
|
|
621
|
+
if (!byteSize)
|
|
622
|
+
throw new RangeError(`${label} is empty`);
|
|
623
|
+
if (byteSize > outputFileLimits.bytes)
|
|
624
|
+
throw new RangeError(`${label} exceeds 25 MiB`);
|
|
625
|
+
const digest = body instanceof Uint8Array ? sha256(body) : body.sha256;
|
|
626
|
+
prepared.push({
|
|
627
|
+
role: file.role,
|
|
628
|
+
filename,
|
|
629
|
+
contentType: file.contentType,
|
|
630
|
+
byteSize,
|
|
631
|
+
sha256: digest,
|
|
632
|
+
body,
|
|
633
|
+
});
|
|
634
|
+
identity = `file:${digest}:${file.contentType}:${filename}`;
|
|
635
|
+
}
|
|
636
|
+
const pinned = `${identity}:${file.role}`;
|
|
637
|
+
if (seen.has(pinned))
|
|
638
|
+
throw new TypeError(`${label} is pinned twice with the role ${file.role}`);
|
|
639
|
+
seen.add(pinned);
|
|
640
|
+
}
|
|
641
|
+
beforeUpload(prepared.map((item) => "artifactId" in item ? item : { artifactId: STAND_IN_ID, role: item.role }));
|
|
642
|
+
const caseKey = sha256(Buffer.from(String(input.externalKey))).slice(0, 16);
|
|
643
|
+
const references = [];
|
|
644
|
+
// A reference and an upload that settle on the same artifact are pinned once: Hue refuses an
|
|
645
|
+
// artifact pinned twice with one role.
|
|
646
|
+
const pins = new Set();
|
|
647
|
+
const pin = (reference) => {
|
|
648
|
+
const identity = `${reference.artifactId.toLowerCase()}:${reference.role}`;
|
|
649
|
+
if (pins.has(identity))
|
|
650
|
+
return;
|
|
651
|
+
pins.add(identity);
|
|
652
|
+
references.push(reference);
|
|
653
|
+
};
|
|
654
|
+
for (const item of prepared) {
|
|
655
|
+
if ("artifactId" in item) {
|
|
656
|
+
pin(item);
|
|
657
|
+
continue;
|
|
658
|
+
}
|
|
659
|
+
const artifactId = await publishArtifact(client, {
|
|
660
|
+
// The key names everything the reservation declares, so a corrected name or type reserves
|
|
661
|
+
// a new artifact instead of colliding with the earlier one.
|
|
662
|
+
idempotencyKey: `hue-sdk:case:${versionId}:${caseKey}:${item.role}:${item.sha256}:${sha256(Buffer.from(item.filename)).slice(0, 16)}:${sha256(Buffer.from(item.contentType)).slice(0, 16)}`,
|
|
663
|
+
filename: item.filename,
|
|
664
|
+
contentType: item.contentType,
|
|
665
|
+
byteSize: item.byteSize,
|
|
666
|
+
sha256: item.sha256,
|
|
667
|
+
}, item.body, `Case file ${item.filename}`, timing);
|
|
668
|
+
pin({ artifactId, role: item.role });
|
|
442
669
|
}
|
|
670
|
+
return references;
|
|
443
671
|
}
|
|
444
672
|
/** Project uploaded staged files as the verified local files handed to scorers. */
|
|
445
673
|
export function localOutputFiles(files) {
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading recorded media back. A trace records a large file as a digest part: the provider part
|
|
3
|
+
* with its content field removed and the file's `sha256` and `size` added, its bytes uploaded
|
|
4
|
+
* under the record's `hue.blobs`. `fetch` downloads one file by digest through a short-lived
|
|
5
|
+
* link Hue issues; `resolve` and {@link rehydrateMedia} put the bytes back into the parts of a
|
|
6
|
+
* recorded value, in the field and encoding each part's shape uses.
|
|
7
|
+
*/
|
|
8
|
+
/** Why a media read failed. */
|
|
9
|
+
export type MediaFetchErrorReason =
|
|
10
|
+
/** Hue holds no such file for the trace or snapshot: an unknown trace, another project's, or a
|
|
11
|
+
* digest none of its records list (with `spanId`, that span's record). */
|
|
12
|
+
"not_found"
|
|
13
|
+
/** The trace lists the file, but its bytes were never uploaded or have been erased. */
|
|
14
|
+
| "missing"
|
|
15
|
+
/** Hue could not read its evidence store, again after one retry; `retryAfterSeconds` says when
|
|
16
|
+
* to ask again. */
|
|
17
|
+
| "unavailable"
|
|
18
|
+
/** The key cannot read recorded content: use a Read or Read and write key, and a Read and write
|
|
19
|
+
* key for a snapshot's copy. */
|
|
20
|
+
| "forbidden"
|
|
21
|
+
/** The file is larger than `maxBytes`; nothing was downloaded. */
|
|
22
|
+
| "too_large"
|
|
23
|
+
/** The downloaded bytes do not have the requested SHA-256 or the size Hue reported. */
|
|
24
|
+
| "digest_mismatch"
|
|
25
|
+
/** A connection failure, a timeout, an unexpected status or an invalid response. */
|
|
26
|
+
| "failed";
|
|
27
|
+
/** Thrown by {@link MediaClient.fetch}. The message is fixed and never includes response text. */
|
|
28
|
+
export declare class MediaFetchError extends Error {
|
|
29
|
+
/** What failed. */
|
|
30
|
+
readonly reason: MediaFetchErrorReason;
|
|
31
|
+
/** HTTP status of the answer that failed, when there was one. */
|
|
32
|
+
readonly status?: number | undefined;
|
|
33
|
+
/** Seconds Hue asked the caller to wait, on `unavailable`, when it said. */
|
|
34
|
+
readonly retryAfterSeconds?: number | undefined;
|
|
35
|
+
constructor(
|
|
36
|
+
/** What failed. */
|
|
37
|
+
reason: MediaFetchErrorReason,
|
|
38
|
+
/** HTTP status of the answer that failed, when there was one. */
|
|
39
|
+
status?: number | undefined,
|
|
40
|
+
/** Seconds Hue asked the caller to wait, on `unavailable`, when it said. */
|
|
41
|
+
retryAfterSeconds?: number | undefined,
|
|
42
|
+
/** `cause`, the error behind a failure; `snapshot`, set when a snapshot's copy was read, so a
|
|
43
|
+
* `forbidden` message names the Read and write key it needs. */
|
|
44
|
+
options?: {
|
|
45
|
+
cause?: unknown;
|
|
46
|
+
snapshot?: boolean;
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
/** One recorded file, downloaded and checked against its digest. */
|
|
50
|
+
export interface MediaFile {
|
|
51
|
+
/** The file's bytes; their SHA-256 is `sha256`. */
|
|
52
|
+
bytes: Uint8Array;
|
|
53
|
+
/** Lowercase hex SHA-256 of `bytes`, as requested. */
|
|
54
|
+
sha256: string;
|
|
55
|
+
/** Byte length of `bytes`. */
|
|
56
|
+
size: number;
|
|
57
|
+
/** The media type the record lists for the file; absent for a snapshot's copy, whose type is
|
|
58
|
+
* the part's own. */
|
|
59
|
+
contentType?: string;
|
|
60
|
+
/** The recorded file name as Hue serves it; absent for a snapshot's copy. */
|
|
61
|
+
fileName?: string;
|
|
62
|
+
}
|
|
63
|
+
/** The file {@link MediaClient.fetch} reads: from a trace, or from a snapshot's pinned copy. */
|
|
64
|
+
export type MediaFetchTarget = {
|
|
65
|
+
/** Hue trace ID (UUID) or the 32-hex OpenTelemetry trace ID. */
|
|
66
|
+
traceId: string;
|
|
67
|
+
/** Lowercase hex SHA-256 the part records. */
|
|
68
|
+
sha256: string;
|
|
69
|
+
/** 16-hex OpenTelemetry span ID of the span that recorded the part. Pass it when known: Hue
|
|
70
|
+
* then reads that span's record alone instead of searching the trace. */
|
|
71
|
+
spanId?: string;
|
|
72
|
+
/** Not set when reading a live trace's file. */
|
|
73
|
+
snapshotId?: undefined;
|
|
74
|
+
} | {
|
|
75
|
+
/** Trace snapshot ID (UUID), such as the one a case converted from a trace keeps. Reading
|
|
76
|
+
* its copy needs a Read and write key. */
|
|
77
|
+
snapshotId: string;
|
|
78
|
+
/** Lowercase hex SHA-256 the part records. */
|
|
79
|
+
sha256: string;
|
|
80
|
+
/** Not set when reading a snapshot's pinned copy. */
|
|
81
|
+
traceId?: undefined;
|
|
82
|
+
/** Not set when reading a snapshot's pinned copy. */
|
|
83
|
+
spanId?: undefined;
|
|
84
|
+
};
|
|
85
|
+
/** Options for {@link MediaClient.fetch}. */
|
|
86
|
+
export interface MediaFetchOptions {
|
|
87
|
+
/** Largest file read into memory, in bytes; a larger one fails with `too_large` before any byte
|
|
88
|
+
* is downloaded. Default 64 MiB, at most 1 GB. */
|
|
89
|
+
maxBytes?: number;
|
|
90
|
+
/** Ends the request, the retry wait and the download. */
|
|
91
|
+
signal?: AbortSignal;
|
|
92
|
+
}
|
|
93
|
+
/** Where {@link MediaClient.resolve} reads the files of a recorded value from, and how it writes
|
|
94
|
+
* them. */
|
|
95
|
+
export type MediaResolveOptions = ({
|
|
96
|
+
/** Hue trace ID (UUID) or the 32-hex OpenTelemetry trace ID the value was recorded in. */
|
|
97
|
+
traceId: string;
|
|
98
|
+
/** 16-hex OpenTelemetry span ID of the span that recorded the value. */
|
|
99
|
+
spanId?: string;
|
|
100
|
+
/** Not set when resolving a live trace's files. */
|
|
101
|
+
snapshotId?: undefined;
|
|
102
|
+
} | {
|
|
103
|
+
/** Trace snapshot ID whose pinned copies hold the files; reading them needs a Read and
|
|
104
|
+
* write key. */
|
|
105
|
+
snapshotId: string;
|
|
106
|
+
/** Not set when resolving a snapshot's pinned copies. */
|
|
107
|
+
traceId?: undefined;
|
|
108
|
+
/** Not set when resolving a snapshot's pinned copies. */
|
|
109
|
+
spanId?: undefined;
|
|
110
|
+
}) & MediaWriteOptions & {
|
|
111
|
+
/** The `hue.blobs` entries of the record the value was read from, in any form Hue reads: an
|
|
112
|
+
* array of compact JSON object strings (what the SDKs write), an array of objects, or one
|
|
113
|
+
* string holding the JSON array. A part that declares no media type, read from a copy that
|
|
114
|
+
* carries none (a snapshot's), takes the type of the entry with its SHA-256 for its `data:`
|
|
115
|
+
* URL, rather than `application/octet-stream`. A part whose content field an entry names and
|
|
116
|
+
* that holds only the first 16 KiB of its file is reported as `prefix_only`. Entries that do
|
|
117
|
+
* not parse are ignored. */
|
|
118
|
+
blobs?: readonly (string | Readonly<Record<string, unknown>>)[] | string;
|
|
119
|
+
/** Files downloaded at once, 1–16. Default 4. */
|
|
120
|
+
concurrency?: number;
|
|
121
|
+
/** Largest file read, as in {@link MediaFetchOptions.maxBytes}. */
|
|
122
|
+
maxBytes?: number;
|
|
123
|
+
/** Ends every request and download. */
|
|
124
|
+
signal?: AbortSignal;
|
|
125
|
+
};
|
|
126
|
+
/** How resolved bytes are written into a part. */
|
|
127
|
+
export interface MediaWriteOptions {
|
|
128
|
+
/**
|
|
129
|
+
* `"dataUrl"` (the default) writes text: a `data:` URL where the part's field takes one (OpenAI
|
|
130
|
+
* `image_url` and `file_data`, Responses `input_image`, `input_file` and `computer_screenshot`,
|
|
131
|
+
* AI SDK `image` and `file` `data`), and bare base64 where its convention requires it (GenAI
|
|
132
|
+
* `blob` `content`, OpenAI `input_audio` and assistant `audio` `data`, Anthropic `source.data`,
|
|
133
|
+
* Gemini `inlineData.data`, Vertex `media` `data`, Bedrock `source.bytes`), so the value can be
|
|
134
|
+
* sent to the provider again. `"bytes"` writes the `Uint8Array` itself in the same field.
|
|
135
|
+
*/
|
|
136
|
+
as?: "dataUrl" | "bytes";
|
|
137
|
+
}
|
|
138
|
+
/** A part {@link MediaClient.resolve} or {@link rehydrateMedia} left unchanged. */
|
|
139
|
+
export interface MediaResolveFailure {
|
|
140
|
+
/** RFC 6901 JSON pointer of the part in the value. */
|
|
141
|
+
pointer: string;
|
|
142
|
+
/** The SHA-256 the part records, or for `prefix_only` the SHA-256 of the entry naming its
|
|
143
|
+
* content field. */
|
|
144
|
+
sha256: string;
|
|
145
|
+
/** Why its bytes were not written: as {@link MediaFetchError.reason}, `not_found` when no local
|
|
146
|
+
* file has the digest, or `prefix_only` for a part whose content field holds only the first
|
|
147
|
+
* 16 KiB of its file, as a `blobs` entry names it; {@link MediaClient.fetch} returns that file
|
|
148
|
+
* whole. */
|
|
149
|
+
reason: MediaFetchErrorReason | "prefix_only";
|
|
150
|
+
}
|
|
151
|
+
/** A recorded value with the bytes of its digest parts written back. */
|
|
152
|
+
export interface MediaResolution<T = unknown> {
|
|
153
|
+
/** A new value: the input is never changed. Each resolved part holds its bytes in its content
|
|
154
|
+
* field and no longer carries `sha256` and `size`. */
|
|
155
|
+
value: T;
|
|
156
|
+
/** Parts whose bytes were written. */
|
|
157
|
+
resolved: number;
|
|
158
|
+
/** Parts left unchanged, in reading order. */
|
|
159
|
+
failures: MediaResolveFailure[];
|
|
160
|
+
}
|
|
161
|
+
/** A local copy of a file {@link rehydrateMedia} reads by digest: a runner's verified case file
|
|
162
|
+
* (`LocalFile`, read from `path`) or bytes already in memory. */
|
|
163
|
+
export type MediaSourceFile = {
|
|
164
|
+
/** Lowercase hex SHA-256 of the file's bytes. */
|
|
165
|
+
sha256: string;
|
|
166
|
+
/** The file's media type, used when the part declares none. */
|
|
167
|
+
contentType?: string;
|
|
168
|
+
} & ({
|
|
169
|
+
/** Path of the file on this machine. */
|
|
170
|
+
path: string;
|
|
171
|
+
/** Not set when the file is given as a path. */
|
|
172
|
+
bytes?: undefined;
|
|
173
|
+
} | {
|
|
174
|
+
/** The file's bytes. */
|
|
175
|
+
bytes: Uint8Array;
|
|
176
|
+
/** Not set when the file is given as bytes. */
|
|
177
|
+
path?: undefined;
|
|
178
|
+
});
|
|
179
|
+
/** The least time a download may go without a byte before it ends: two minutes. Tests shorten it;
|
|
180
|
+
* it is not exported from the package. */
|
|
181
|
+
export declare const mediaDownload: {
|
|
182
|
+
minIdleMillis: number;
|
|
183
|
+
};
|
|
184
|
+
/**
|
|
185
|
+
* Hue's media reads, `client.media`: one recorded file by digest (`fetch`), and every digest part
|
|
186
|
+
* of a recorded value written back with its bytes (`resolve`). The project key is sent to Hue
|
|
187
|
+
* only; the file itself comes from a short-lived link fetched without it.
|
|
188
|
+
*/
|
|
189
|
+
export declare class MediaClient {
|
|
190
|
+
#private;
|
|
191
|
+
/**
|
|
192
|
+
* Downloads one recorded file and checks it against its digest: from a trace (Hue reads the
|
|
193
|
+
* span's record that lists the digest under `hue.blobs`) or from a snapshot's pinned copy. A
|
|
194
|
+
* read Hue answers as unavailable (503 or 429) is asked once more after its `Retry-After`, at
|
|
195
|
+
* most 10 seconds, and so is one that lost its connection or timed out.
|
|
196
|
+
*
|
|
197
|
+
* @throws MediaFetchError for a file Hue does not hold, a refused key, an unavailable store, a
|
|
198
|
+
* file over `maxBytes`, bytes that do not match the digest or a failed request.
|
|
199
|
+
* @throws TypeError for an invalid trace, span or snapshot ID or digest.
|
|
200
|
+
*/
|
|
201
|
+
fetch(target: MediaFetchTarget, options?: MediaFetchOptions): Promise<MediaFile>;
|
|
202
|
+
/**
|
|
203
|
+
* Writes the bytes of every digest part in `value` back into its content field, the reverse of
|
|
204
|
+
* the SDK's media capture: each distinct digest is downloaded once with {@link fetch}, at most
|
|
205
|
+
* `concurrency` at a time. Returns a new value; a part whose file cannot be read stays as it is
|
|
206
|
+
* and is reported in `failures`. Parse attributes recorded as JSON text (such as
|
|
207
|
+
* `gen_ai.input.messages`) before passing them; pass the record's `hue.blobs` as `blobs` when
|
|
208
|
+
* reading a snapshot, whose copies carry no media type.
|
|
209
|
+
*
|
|
210
|
+
* @throws TypeError for invalid identifiers or options; never for one file's failure.
|
|
211
|
+
*/
|
|
212
|
+
resolve<T>(value: T, options: MediaResolveOptions): Promise<MediaResolution<T>>;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Writes case files back into the digest parts of a case's inputs, by SHA-256: the files a runner
|
|
216
|
+
* hands a target (`context.files`) or any bytes the caller holds, so a target can send the
|
|
217
|
+
* recorded model call again with the real image, document or audio. Each file is read once and
|
|
218
|
+
* checked against its digest; a part with no matching file, or whose file does not match, stays
|
|
219
|
+
* as it is and is reported in `failures`. Returns a new value; the input is never changed.
|
|
220
|
+
*/
|
|
221
|
+
export declare function rehydrateMedia<T>(value: T, files: readonly MediaSourceFile[], options?: MediaWriteOptions): Promise<MediaResolution<T>>;
|