@hue-run/sdk 0.17.0 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLI.md +8 -8
- package/EVALUATIONS.md +53 -2
- package/README.md +262 -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/client.d.ts +18 -0
- package/dist/client.js +148 -13
- package/dist/evals/client.d.ts +14 -1
- package/dist/evals/client.js +73 -9
- package/dist/evals/files.d.ts +11 -1
- package/dist/evals/files.js +160 -20
- package/dist/evals/media.d.ts +217 -0
- package/dist/evals/media.js +907 -0
- package/dist/evals/runner.js +130 -106
- package/dist/evals/types.d.ts +48 -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 +131 -8
- package/dist/inline-files.js +565 -47
- 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/snapshot.d.ts +1 -1
- package/dist/snapshot.js +123 -29
- package/dist/transport.d.ts +7 -0
- package/dist/transport.js +285 -55
- 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/CLI.md
CHANGED
|
@@ -42,7 +42,7 @@ project manifest are refused because managers can update ancestor locks; Python
|
|
|
42
42
|
The generated `hue.setup.mjs` or `hue_setup.py` always selects `captureContent: false` /
|
|
43
43
|
`capture_content=False`. For a supported application, setup installs the dependency and adds the
|
|
44
44
|
managed import and middleware registration to the existing entrypoint; an unreferenced helper is
|
|
45
|
-
not a completed integration. TypeScript uses `@hue-run/sdk@0.
|
|
45
|
+
not a completed integration. TypeScript uses `@hue-run/sdk@0.18.0`, `@opentelemetry/api@1.9.1` and
|
|
46
46
|
`@opentelemetry/context-async-hooks@2.11.0`; Python setup uses its separately tested package pin.
|
|
47
47
|
Content capture requires an ordinary account-managed key and a later explicit application decision.
|
|
48
48
|
|
|
@@ -469,7 +469,7 @@ node packages/sdk-typescript/scripts/verify-package.mjs --artifacts-dir .artifac
|
|
|
469
469
|
# Set project to an existing supported fixture; use the same directory on resume.
|
|
470
470
|
project=/absolute/path/to/supported-fixture
|
|
471
471
|
node packages/sdk-typescript/scripts/verify-setup-live.mjs \
|
|
472
|
-
--archive .artifacts/typescript/hue-run-sdk-0.
|
|
472
|
+
--archive .artifacts/typescript/hue-run-sdk-0.18.0.tgz \
|
|
473
473
|
--origin https://STAGING_ORIGIN \
|
|
474
474
|
--project "$project" --command setup \
|
|
475
475
|
--evidence .context/setup-staging-before-claim.json
|
|
@@ -480,7 +480,7 @@ the private local handoff and finish the real browser claim, then reconcile the
|
|
|
480
480
|
|
|
481
481
|
```sh
|
|
482
482
|
node packages/sdk-typescript/scripts/verify-setup-live.mjs \
|
|
483
|
-
--archive .artifacts/typescript/hue-run-sdk-0.
|
|
483
|
+
--archive .artifacts/typescript/hue-run-sdk-0.18.0.tgz \
|
|
484
484
|
--origin https://STAGING_ORIGIN \
|
|
485
485
|
--project "$project" --command claim \
|
|
486
486
|
--evidence .context/setup-staging-after-claim.json
|
|
@@ -522,10 +522,10 @@ otherwise. Never hand the agent Hue-native tools (`context.tools`, `context.mcp`
|
|
|
522
522
|
production agent lacks. Pin the SDK, so the agent's repository gains no dependency:
|
|
523
523
|
|
|
524
524
|
```sh
|
|
525
|
-
npx -y @hue-run/sdk@0.
|
|
526
|
-
npx -y @hue-run/sdk@0.
|
|
527
|
-
npx -y @hue-run/sdk@0.
|
|
528
|
-
npx -y @hue-run/sdk@0.
|
|
525
|
+
npx -y @hue-run/sdk@0.18.0 eval --case "Refund an eligible charge" --command "node agent.js"
|
|
526
|
+
npx -y @hue-run/sdk@0.18.0 eval --case https://app.hue.run/projects/demo/scenarios/<id> --command "python agent.py" --timeout 120 --baseline <run id>
|
|
527
|
+
npx -y @hue-run/sdk@0.18.0 eval --set "Billing regressions" --scorer-version <id> --command "node agent.js" --save-version
|
|
528
|
+
npx -y @hue-run/sdk@0.18.0 eval --worker --command "node agent.js" --agent-key support-agent --revision <new-agent-revision>
|
|
529
529
|
```
|
|
530
530
|
|
|
531
531
|
One invocation creates one run holding every selected case; it prints the run URL
|
|
@@ -563,7 +563,7 @@ export default function runMyAgent(inputs: JsonValue, context: SimulationTargetC
|
|
|
563
563
|
```
|
|
564
564
|
|
|
565
565
|
```sh
|
|
566
|
-
npx -y @hue-run/sdk@0.
|
|
566
|
+
npx -y @hue-run/sdk@0.18.0 eval --case "Refund an eligible charge" ./hue-agent.ts --content
|
|
567
567
|
```
|
|
568
568
|
|
|
569
569
|
`--scenario` remains an alias for `--case` for existing scripts. Pass one selection flag.
|
package/EVALUATIONS.md
CHANGED
|
@@ -295,13 +295,63 @@ an `error` of type `TargetTimeout` and the world is sealed abandoned. Public pac
|
|
|
295
295
|
proves this lifecycle against local fixtures; exact installed-registry-package to hosted-facade
|
|
296
296
|
acceptance remains a post-publication Fern gate.
|
|
297
297
|
|
|
298
|
+
## Case files
|
|
299
|
+
|
|
300
|
+
`addCase` and `addEvalSetCase` pin up to 32 input files with `files`. Name an artifact the
|
|
301
|
+
project already holds as `{ artifactId, role }`, or give the file, from `bytes` or a `path`, and
|
|
302
|
+
the client uploads it as a verified artifact first:
|
|
303
|
+
|
|
304
|
+
```ts
|
|
305
|
+
await client.addEvalSetCase(versionId, {
|
|
306
|
+
expectedRevision,
|
|
307
|
+
externalKey: "chart-question",
|
|
308
|
+
inputs: { messages },
|
|
309
|
+
files: [
|
|
310
|
+
{ role: "source", filename: "chart.png", contentType: "image/png", bytes: chart },
|
|
311
|
+
{ role: "evaluator_reference", filename: "answers.csv", contentType: "text/csv", path: "answers.csv" },
|
|
312
|
+
],
|
|
313
|
+
});
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
An uploaded file needs one of `outputContentTypes`, a well-formed name of 1–255 UTF-16 code units
|
|
317
|
+
(as JavaScript counts a string's `length`) without path separators or control characters, and 1
|
|
318
|
+
byte to 25 MiB. Every file, and the case itself as it will be sent (its revision, key, values and
|
|
319
|
+
size), is checked before the first upload; a file outside that policy, a `path` that cannot be
|
|
320
|
+
read, or a case Hue would refuse throws `TypeError` or `RangeError` without a request. The
|
|
321
|
+
reservation key names the version, the case's `externalKey`, the role, the bytes, the name and the
|
|
322
|
+
content type, so sending the same case again reuses its artifacts and a corrected name or type
|
|
323
|
+
reserves a new one; a reference to the artifact a file uploads as pins it once. `source`, `attached_template`, `attached_reference` and `original` files reach
|
|
324
|
+
the target as `context.files`; `org_template` and `evaluator_reference` files reach only scorers.
|
|
325
|
+
|
|
326
|
+
Recorded messages copied into a case's inputs keep their large files as digest parts. Pin each file
|
|
327
|
+
to the case, and the target can send the recorded model call again with the real image, document
|
|
328
|
+
or audio:
|
|
329
|
+
|
|
330
|
+
```ts
|
|
331
|
+
import { rehydrateMedia, runExperiment } from "@hue-run/sdk/evals";
|
|
332
|
+
|
|
333
|
+
await runExperiment({
|
|
334
|
+
// ...client, hue, experimentId, checkpointDirectory and the other options
|
|
335
|
+
target: async (inputs, { files }) => {
|
|
336
|
+
const { value } = await rehydrateMedia(inputs, files);
|
|
337
|
+
return callModel(value);
|
|
338
|
+
},
|
|
339
|
+
});
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
`rehydrateMedia(value, files, { as })` matches digest parts to files by SHA-256, checks each
|
|
343
|
+
file's bytes against the digest and writes them as `client.media.resolve` does (see the
|
|
344
|
+
[README](README.md#read-recorded-media-back)); `files` may also hold `{ sha256, contentType, bytes }`
|
|
345
|
+
entries. A part without a matching file stays as it is and is listed in `failures`.
|
|
346
|
+
|
|
298
347
|
## Direct cases and files
|
|
299
348
|
|
|
300
349
|
Cases without a simulated world — answer-only tasks, or document workflows with pinned files —
|
|
301
350
|
run on the same runner under the same checkpoint rules. They shipped in `@hue-run/sdk` `0.5.0`
|
|
302
351
|
and require a Hue deployment that serves case `inputFiles` on experiment
|
|
303
352
|
items, subject `files`, and the artifact reservation, upload, completion and download APIs. The
|
|
304
|
-
Python SDK
|
|
353
|
+
Python SDK's `run_experiment` and `rescore` hand pinned input files to targets and scorers;
|
|
354
|
+
generated files and the outbound worker are TypeScript-only.
|
|
305
355
|
|
|
306
356
|
Since TypeScript 0.13.2, the `hue eval --worker --command "…"` wrapper supplies both callbacks, so one
|
|
307
357
|
command worker accepts direct and world cases, including mixed eval sets. It keeps the launch
|
|
@@ -380,7 +430,8 @@ evaluator material must stay out of its reach. Verified copies live under `files
|
|
|
380
430
|
`<checkpointDirectory>/files`, created mode 0700), evaluator-only files apart from the agent's, and
|
|
381
431
|
each case gets its own private `context.outputDirectory` to write into. The files of a case pinned
|
|
382
432
|
to a world are removed when it ends, except staged outputs an interrupted upload resumes from;
|
|
383
|
-
those of a direct case stay for inspection
|
|
433
|
+
those of a direct case stay for inspection, except its evaluator-only files, which are removed when
|
|
434
|
+
it ends so no later case's agent finds them.
|
|
384
435
|
|
|
385
436
|
Return generated documents by wrapping the output in `withFiles(output, files)`. Each entry names a
|
|
386
437
|
`path` or in-memory `bytes`, a `filename`, a Hue-accepted `contentType` and at most one `primary`.
|
package/README.md
CHANGED
|
@@ -202,7 +202,172 @@ helpers (`setInput`, `setOutput`, `tool` arguments and results, `recordMessages`
|
|
|
202
202
|
that is not JSON, such as a `Date` or a class instance, is omitted with an instrumentation failure
|
|
203
203
|
while the callback result is returned unchanged.
|
|
204
204
|
|
|
205
|
-
##
|
|
205
|
+
## Images, documents and audio
|
|
206
|
+
|
|
207
|
+
Hue stores the bytes of images, documents and audio apart from the spans and log records that
|
|
208
|
+
mention them, under the record's trace and named by SHA-256. The record keeps the file's digest in
|
|
209
|
+
its place and lists the upload in its `hue.blobs` attribute. Files reach Hue in four ways:
|
|
210
|
+
|
|
211
|
+
| You have | Use | The record carries | `hue.blobs` key |
|
|
212
|
+
| --- | --- | --- | --- |
|
|
213
|
+
| Files inline in the messages and values your code or framework already records | Nothing: [automatic capture](#automatically-captured-media) | The part, in digest form | `<attribute>#<JSON pointer>` |
|
|
214
|
+
| A file to place at a position in a value you record | [`Attachment`](#attach-a-file-anywhere) | A GenAI `blob` part, in digest form | `<attribute>#<JSON pointer>`, or `body#<JSON pointer>` in a `recordMessages()` log record |
|
|
215
|
+
| A file the work read, received or produced, outside any recorded value | [`hue.recordFile`](#record-a-file) | A `hue.file` event | `event:hue.file:<sha256>` |
|
|
216
|
+
| Files a case's agent and scorers receive in an evaluation | [`CaseWrite.files`](#files-in-evaluation-cases) | An input file pinned on the case | None: case files are artifacts |
|
|
217
|
+
|
|
218
|
+
[`client.media`](#read-recorded-media-back) reads a recorded file back. Every upload needs
|
|
219
|
+
`captureContent: true` and a project key (never a setup credential, `hue_setup_…`) and follows
|
|
220
|
+
`redact`: nothing the redactor removes is uploaded, a value it rewrites uploads as rewritten, and an
|
|
221
|
+
attachment part it rewrites uploads nothing. Uploads share the export's upload budget and the 128
|
|
222
|
+
MiB held for values awaiting upload, and fall back without failing the export; a file used more than
|
|
223
|
+
once in a trace is stored once. See [Values over the inline limit](#values-over-the-inline-limit)
|
|
224
|
+
for the upload protocol, budgets and fallbacks.
|
|
225
|
+
|
|
226
|
+
### Automatically captured media
|
|
227
|
+
|
|
228
|
+
With `captureContent: true` and a project key, the SDK finds the files inline in the attributes Hue
|
|
229
|
+
reads files from, uploads each file's bytes under the record's trace and leaves the file's digest in
|
|
230
|
+
its place. On a span's own attributes:
|
|
231
|
+
|
|
232
|
+
- Messages and values: `gen_ai.input.messages`, `gen_ai.output.messages`, `ai.prompt.messages`,
|
|
233
|
+
the AI SDK's `ai.prompt`, `input.value` and `output.value` (what `withSpan` and OpenInference
|
|
234
|
+
record), `traceloop.entity.input` and `traceloop.entity.output`, and Langfuse's
|
|
235
|
+
`langfuse.observation.input`, `langfuse.observation.output`, `langfuse.trace.input` and
|
|
236
|
+
`langfuse.trace.output`.
|
|
237
|
+
- Tool arguments and results: `gen_ai.tool.call.arguments`, `gen_ai.tool.call.result`,
|
|
238
|
+
`ai.toolCall.args`, `ai.toolCall.result`, `tool.parameters`, `tool_arguments` and
|
|
239
|
+
`tool_response`.
|
|
240
|
+
- Metadata: `metadata`, `langfuse.observation.metadata` and `langfuse.trace.metadata` (whole, or
|
|
241
|
+
one `.<key>` per attribute) and `ai.telemetry.metadata.<key>`. A metadata value is read as JSON
|
|
242
|
+
when it is an object or array; otherwise it is a file only when the whole value is a `data:` URL.
|
|
243
|
+
- Indexed messages: OpenLLMetry's `gen_ai.prompt.<n>.content` and `gen_ai.completion.<n>.content`
|
|
244
|
+
when the value is the JSON of a parts list (any other content stays text), and OpenInference's
|
|
245
|
+
flattened `llm.input_messages.<n>.message.contents.<m>.message_content.image.image.url` (and
|
|
246
|
+
`llm.output_messages.…`), whose value is the image's URL.
|
|
247
|
+
|
|
248
|
+
Inside those values a file is found by its shape: GenAI `blob`, AI SDK `file` and `image`, OpenAI
|
|
249
|
+
Chat image, file and audio parts and an assistant message's `audio`, Responses `input_image`,
|
|
250
|
+
`input_file` and `computer_screenshot`, Anthropic base64 sources, Gemini `inlineData` and
|
|
251
|
+
`inline_data`, Vertex `media`, Bedrock image and document sources, and any whole string that is a
|
|
252
|
+
`data:` URL declaring a media type. A tool call's `arguments` and a tool result (a `tool` message's
|
|
253
|
+
`content`, `tool_result`, `function_call_output`, `tool_call_response`) are JSON in a string by
|
|
254
|
+
convention, so the files inside them are uploaded too; a message's own text is never parsed as JSON.
|
|
255
|
+
Hue's reader looks for files no deeper than ten levels below a value's root (an encoded string's
|
|
256
|
+
JSON counting as a level), and the SDK inspects no deeper. Hue keeps attribute JSON nested past 32
|
|
257
|
+
levels, or holding an integer past 2^53, as text, and an OpenInference `input.value` or
|
|
258
|
+
`output.value` whose record declares `input.mime_type` or `output.mime_type` as `text/plain` is
|
|
259
|
+
literal text: the SDK leaves such values as recorded. Remote URLs, provider file IDs, S3 references
|
|
260
|
+
and Langfuse media tokens stay unchanged, and Hue never fetches them.
|
|
261
|
+
|
|
262
|
+
Each uploaded file is listed in the record's `hue.blobs`, keyed `<attribute>#<JSON pointer>`:
|
|
263
|
+
|
|
264
|
+
- A provider part loses its byte field and carries `sha256` and `size`; the key names the part,
|
|
265
|
+
for example `gen_ai.input.messages#/0/parts/1`.
|
|
266
|
+
- An OpenAI assistant message's `audio` loses `audio.data` and the message carries `sha256` and
|
|
267
|
+
`size` (`gen_ai.output.messages#/0`). Its media type comes from `audio.format`, else from the
|
|
268
|
+
bytes' WAV, MP3, AAC, FLAC or Ogg signature, else `application/octet-stream`.
|
|
269
|
+
- A whole `data:` URL string becomes a GenAI `blob` part,
|
|
270
|
+
`{"type":"blob","mime_type":"image/png","modality":"image","sha256":"…","size":68}`, keyed at
|
|
271
|
+
the string (`input.value#/image`). A value that is entirely one, such as a metadata attribute,
|
|
272
|
+
becomes that part's JSON text, keyed `<attribute>#`.
|
|
273
|
+
- A file inside a JSON-encoded tool string takes the digest form inside the string. Its key goes on
|
|
274
|
+
through the string, which no JSON pointer can address, so Hue matches it by its digest.
|
|
275
|
+
- Text media over 64 KiB of decoded bytes in a provider part keeps a UTF-8-safe 16 KiB prefix in
|
|
276
|
+
its content field instead, and its key names that field
|
|
277
|
+
(`gen_ai.input.messages#/0/parts/1/content`). Smaller text and empty fields stay inline.
|
|
278
|
+
|
|
279
|
+
Only the file's own value, or the members of the part that held it, change: every other byte of the
|
|
280
|
+
recorded text, its key order, number spellings, escapes and spacing included, is kept, in an encoded
|
|
281
|
+
string as in the attribute itself, so both SDKs write the same bytes. A record has 64 `hue.blobs`
|
|
282
|
+
places: the media inline in its values take them first, in reading order, then its attachments and
|
|
283
|
+
the files recorded with `upload`.
|
|
284
|
+
|
|
285
|
+
Entries use the recorded media type and, when valid, the filename, name or title. A valid name
|
|
286
|
+
is nonblank after trimming, at most 256 UTF-16 code units and well-formed Unicode, with no C0
|
|
287
|
+
characters (U+0000–U+001F) or DEL (U+007F). Invalid names are omitted from the entry without
|
|
288
|
+
rewriting the original field. Data URLs and fields explicitly encoded as base64 by the provider
|
|
289
|
+
contract are decoded before hashing. Other non-data strings with a valid declared textual media
|
|
290
|
+
type remain raw UTF-8: `YWJj` hashes as those four characters. The digest matches
|
|
291
|
+
`hue.recordFile`'s digest for the same bytes.
|
|
292
|
+
|
|
293
|
+
A file that is not uploaded (the upload failed, the receiver has no upload route, or a budget ran
|
|
294
|
+
out) keeps its digest without an entry in the recorded messages (`gen_ai.input.messages`,
|
|
295
|
+
`gen_ai.output.messages`, `ai.prompt.messages`); in every other attribute it keeps its bytes
|
|
296
|
+
inline under the ordinary value limit.
|
|
297
|
+
|
|
298
|
+
Log records upload their media the same way when they are correlated with a span:
|
|
299
|
+
`recordMessages`'s `gen_ai.client.inference.operation.details` record and any `gen_ai.*` event
|
|
300
|
+
with a structured body. The bytes upload under the log record's own trace and the log record lists
|
|
301
|
+
them in its own `hue.blobs`, keyed `body#<JSON pointer>` into the body
|
|
302
|
+
(`body#/gen_ai.input.messages/0/parts/1`, or `body#/content/1` for a deprecated
|
|
303
|
+
`gen_ai.user.message` event) or `<attribute>#<JSON pointer>` for the attributes above. One step
|
|
304
|
+
places a log record's files, the attachments in its body included. A body recorded as JSON text
|
|
305
|
+
stays as it is, and no log value is uploaded whole. A log file that is not uploaded is placed as
|
|
306
|
+
log records were before they uploaded: a file over 64 KiB in a recorded message attribute becomes
|
|
307
|
+
its digest and any other file keeps its bytes; text still over the value limit is cut inline and
|
|
308
|
+
listed under `hue.truncated`. While uploads are available, `recordMessages` records a body over
|
|
309
|
+
the value limit when it fits the limit with its files as their digest.
|
|
310
|
+
|
|
311
|
+
Inspection is bounded to 8 Mi UTF-16 code units of text per attribute and 16 Mi per record, a log
|
|
312
|
+
body's file contents included; a longer value uses the ordinary value limit. Inspection parses the
|
|
313
|
+
value and decodes each file while the record is queued and again when it is exported, holding
|
|
314
|
+
several copies of the value at once; larger files belong in an `Attachment` or in
|
|
315
|
+
`hue.recordFile` with `upload` set, whose bytes are not parsed out of text. Resources, links and
|
|
316
|
+
span events never upload inline media: in their recorded messages, media up to 64 KiB stays
|
|
317
|
+
verbatim and larger media is replaced by its digest, as in 0.16, and log records that cannot
|
|
318
|
+
upload (not correlated with a span, or while uploads are unavailable) keep the same rule. With
|
|
319
|
+
`captureContent: false`, nothing is inspected or uploaded.
|
|
320
|
+
|
|
321
|
+
### Attach a file anywhere
|
|
322
|
+
|
|
323
|
+
Wrap a file in an `Attachment` and put it anywhere in a value Hue records: a `withSpan()` input or
|
|
324
|
+
output, `model()` messages, a tool's arguments or result, `recordMessages()` messages or a span
|
|
325
|
+
attribute passed to `withSpan()`.
|
|
326
|
+
|
|
327
|
+
```ts
|
|
328
|
+
import { readFile } from "node:fs/promises";
|
|
329
|
+
import { Attachment } from "@hue-run/sdk";
|
|
330
|
+
|
|
331
|
+
const png = await readFile("scan.png");
|
|
332
|
+
await hue.withSpan(
|
|
333
|
+
"review contract",
|
|
334
|
+
(span) => {
|
|
335
|
+
span.setInput({
|
|
336
|
+
question: "Which clauses changed?",
|
|
337
|
+
contract: new Attachment({ path: "contract.pdf", contentType: "application/pdf" }),
|
|
338
|
+
});
|
|
339
|
+
},
|
|
340
|
+
{ attributes: { metadata: { scan: new Attachment({ data: png, contentType: "image/png" }) } } },
|
|
341
|
+
);
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
Give exactly one of `data` (a `Buffer`, typed array, `ArrayBuffer`, or a base64 string or `data:`
|
|
345
|
+
URL) and `path` (read from the file system when the attachment is created), a `contentType` and
|
|
346
|
+
optionally a `name`. The bytes are read and hashed when the attachment is created and are limited
|
|
347
|
+
to 25 MiB, the `recordFile` limit: they are held in memory from then until export, within the
|
|
348
|
+
128 MiB budget that queued spans share for values awaiting upload.
|
|
349
|
+
|
|
350
|
+
At its position the attachment is recorded as a GenAI `blob` part that names the file by digest,
|
|
351
|
+
`{"type":"blob","mime_type":"application/pdf","modality":"document","sha256":"<64 hex>","size":48213,"name":"contract.pdf"}`,
|
|
352
|
+
with `modality` read from the content type (`image`, `audio`, `video`, `document` or `file`).
|
|
353
|
+
Export uploads the bytes and lists them under the record's `hue.blobs`, keyed
|
|
354
|
+
`<attribute>#<JSON pointer to the part>`: `input.value#/contract` above, `metadata#/scan` for the
|
|
355
|
+
attribute, and `body#/gen_ai.input.messages/0/parts/1` for a part in a `recordMessages()` log
|
|
356
|
+
record, which uploads with the other media in the body. An attribute value that is an attachment,
|
|
357
|
+
a JSON object or an array OpenTelemetry cannot hold (not of one primitive type), with or without
|
|
358
|
+
attachments, is recorded as JSON text; put files under `metadata` for Hue to list them as the
|
|
359
|
+
span's metadata files.
|
|
360
|
+
|
|
361
|
+
With `captureContent: false` or a setup credential no bytes leave the process: a metadata
|
|
362
|
+
attribute still records the part, without its name, and content values are not recorded at all. A
|
|
363
|
+
part the redactor removes or rewrites is not uploaded. A part that cannot be uploaded keeps its
|
|
364
|
+
digest and is counted in `report.uploadFallbacks`, as is one nested more than 10 levels deep in its
|
|
365
|
+
value, where Hue does not read parts, or one in a value cut to the value limit. Creating an
|
|
366
|
+
attachment never throws: one without exactly one of `data` and `path`, with unreadable or larger
|
|
367
|
+
data or an invalid content type is recorded as `null` and counted as an instrumentation failure;
|
|
368
|
+
an invalid name alone is omitted and counted.
|
|
369
|
+
|
|
370
|
+
### Record a file
|
|
206
371
|
|
|
207
372
|
Record a file the work read, received or produced with `hue.recordFile`. It adds a `hue.file`
|
|
208
373
|
event to the active span, or to the span of an explicit context passed second. The event identifies
|
|
@@ -223,7 +388,9 @@ await hue.withSpan("review contract", async () => {
|
|
|
223
388
|
`data` is limited to 25 MiB. Larger data is omitted and counted as an instrumentation failure;
|
|
224
389
|
pass `sha256` and `byteSize` when you already have them. Upload requires supplied bytes, content
|
|
225
390
|
capture and an ordinary project key. It runs during export and adds a `hue.blobs` entry to the
|
|
226
|
-
same span, keyed `event:hue.file:<sha256>`, while preserving the event.
|
|
391
|
+
same span, keyed `event:hue.file:<sha256>`, while preserving the event. An export fits every
|
|
392
|
+
span's events and entries before it uploads any of these files, then uploads the files of all its
|
|
393
|
+
spans together, four at a time; a file whose event was redacted or shed is not uploaded.
|
|
227
394
|
|
|
228
395
|
The event carries `hue.file.sha256`, `hue.file.role` (`input`, `attachment` or `output`),
|
|
229
396
|
`hue.file.media_type` and `hue.file.size` in both capture modes. `hue.file.name` requires
|
|
@@ -233,6 +400,88 @@ Tabs, newlines, DEL and lone surrogates make a name invalid. An invalid name is
|
|
|
233
400
|
counted as an instrumentation failure; the otherwise valid event remains. Invalid records and
|
|
234
401
|
records without an active span count as instrumentation failures.
|
|
235
402
|
|
|
403
|
+
### Read recorded media back
|
|
404
|
+
|
|
405
|
+
The evaluation client's `media` reads a recorded file back by its digest, with a key that may read
|
|
406
|
+
recorded content (**Read** or **Read and write**; a trace snapshot's copy needs **Read and write**):
|
|
407
|
+
|
|
408
|
+
```ts
|
|
409
|
+
import { createEvaluationClient } from "@hue-run/sdk/evals";
|
|
410
|
+
|
|
411
|
+
const client = createEvaluationClient({ apiKey: process.env.HUE_API_KEY! });
|
|
412
|
+
|
|
413
|
+
// One file by digest; pass the span that recorded it when you know it.
|
|
414
|
+
const file = await client.media.fetch({ traceId, spanId, sha256 });
|
|
415
|
+
console.log(file.contentType, file.fileName, file.size, file.bytes);
|
|
416
|
+
|
|
417
|
+
// Every digest part of a recorded value, written back in its own field.
|
|
418
|
+
const recorded = JSON.parse(attributes["gen_ai.input.messages"]);
|
|
419
|
+
const { value: messages, failures } = await client.media.resolve(recorded, { traceId, spanId });
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
`fetch` asks Hue for a five-minute download link (`GET /api/v1/traces/{traceId}/blobs/{sha256}`),
|
|
423
|
+
downloads it without the project key and refuses bytes whose SHA-256 is not the digest. It throws
|
|
424
|
+
`MediaFetchError`, whose `reason` is `not_found` (Hue holds no such file for the trace),
|
|
425
|
+
`missing` (the trace lists it, but it was never uploaded or has been erased), `unavailable` (still
|
|
426
|
+
unavailable after one retry that waits out a `Retry-After` of up to 10 seconds), `forbidden` (the
|
|
427
|
+
key cannot read recorded content, or reads a snapshot without **Read and write**), `too_large`
|
|
428
|
+
(over `maxBytes`, 64 MiB by default, checked before downloading), `digest_mismatch` or `failed` (a
|
|
429
|
+
connection failure, a timeout, a redirect or another unexpected answer). Hue is asked for the link
|
|
430
|
+
once more after a connection failure or a timeout, never after an unexpected answer; a download
|
|
431
|
+
ends once no byte has arrived for the client's timeout, at least two minutes, and is not retried.
|
|
432
|
+
`{ snapshotId, sha256 }` reads a trace snapshot's pinned copy instead
|
|
433
|
+
(`GET /api/v1/trace-snapshots/{snapshotId}/blobs/{sha256}`), which carries no media type or name.
|
|
434
|
+
|
|
435
|
+
`resolve` downloads each distinct digest once, four at a time (`concurrency`, up to 16), and returns
|
|
436
|
+
a new value; the input is not changed. With `as: "dataUrl"` (the default) each part receives a
|
|
437
|
+
`data:` URL where its field takes one (OpenAI `image_url` and `file_data`, Responses `input_image`,
|
|
438
|
+
`input_file` and `computer_screenshot`, AI SDK `image` and `file`) and bare base64 where its
|
|
439
|
+
convention requires it (GenAI `blob`, OpenAI `input_audio` and assistant `audio`, Anthropic
|
|
440
|
+
`source.data`, Gemini `inlineData`, Vertex `media`, Bedrock `source.bytes`), so the messages can be
|
|
441
|
+
sent to the provider again. A GenAI `blob` or Vertex `media` part receives base64 whatever its media
|
|
442
|
+
type, as the GenAI conventions define its content and as Hue reads content that reads as base64;
|
|
443
|
+
`as: "bytes"` writes a `Uint8Array`. A resolved part no longer carries `sha256` and `size`. A part
|
|
444
|
+
whose file cannot be read stays as it is and is listed in `failures` with its JSON pointer and
|
|
445
|
+
reason. Parts inside JSON-encoded strings, such as tool-call arguments, and Langfuse media tokens,
|
|
446
|
+
which name files Langfuse holds, are left as they are; `fetch` reads the former by the digest the
|
|
447
|
+
part carries.
|
|
448
|
+
|
|
449
|
+
A snapshot's copies carry no media type, so a part that declares none would get a `data:` URL of
|
|
450
|
+
`application/octet-stream`. Pass the record's `hue.blobs` entries in any form Hue reads (the JSON
|
|
451
|
+
strings the attribute holds, objects, or one string holding the JSON array), and such a part takes
|
|
452
|
+
the type of the entry with its SHA-256. Text over 64 KiB that a record keeps as its first 16 KiB
|
|
453
|
+
is never written back: a part whose content field an entry names, holding only that prefix, stays
|
|
454
|
+
as it is and is listed in `failures` as `prefix_only` with the entry's SHA-256, and
|
|
455
|
+
`fetch({ traceId, sha256 })` returns the whole file. An entry names a pointer, not a value, so the
|
|
456
|
+
entries of another attribute with a file at the same pointer are listed too; pass only the entries
|
|
457
|
+
of the value's own attribute to list its own file.
|
|
458
|
+
|
|
459
|
+
```ts
|
|
460
|
+
const { value } = await client.media.resolve(recorded, {
|
|
461
|
+
snapshotId,
|
|
462
|
+
blobs: attributes["hue.blobs"],
|
|
463
|
+
});
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
### Files in evaluation cases
|
|
467
|
+
|
|
468
|
+
`addCase` and `addEvalSetCase` pin up to 32 input files on a case with `files`: artifacts the
|
|
469
|
+
project already holds, or bytes and paths the client checks and uploads first. A case's target
|
|
470
|
+
receives its agent-visible files as `context.files`, and `rehydrateMedia(inputs, files)` writes
|
|
471
|
+
them back into the digest parts of recorded messages copied into the case, so the target can send
|
|
472
|
+
the recorded model call again with the real image, document or audio. See
|
|
473
|
+
[Case files](EVALUATIONS.md#case-files).
|
|
474
|
+
|
|
475
|
+
### Coming from Langfuse or Braintrust
|
|
476
|
+
|
|
477
|
+
| Langfuse | Braintrust | Hue |
|
|
478
|
+
| --- | --- | --- |
|
|
479
|
+
| Base64 data URIs in an observation's input, output or metadata | | [Automatic capture](#automatically-captured-media), for the attributes and shapes listed there |
|
|
480
|
+
| `LangfuseMedia` | `Attachment` | [`Attachment`](#attach-a-file-anywhere), at the same position |
|
|
481
|
+
| `resolveMediaReferences({ obj, resolveWith: "base64DataUri" })` | | `client.media.resolve(obj, { traceId })` |
|
|
482
|
+
| `GET /api/public/media/{mediaId}` | `ReadonlyAttachment.data()` | `client.media.fetch({ traceId, sha256 })` |
|
|
483
|
+
| Media tokens (`@@@langfuseMedia:…@@@`) | `ExternalAttachment` | Left as recorded: Hue never fetches a remote URL or a file another service holds |
|
|
484
|
+
|
|
236
485
|
## Vercel AI SDK 6
|
|
237
486
|
|
|
238
487
|
AI SDK 6 accepts a per-call tracer through `experimental_telemetry`. Pass
|
|
@@ -376,34 +625,8 @@ definition list. The digest is the same in both SDKs and does not change when a
|
|
|
376
625
|
rotates. Only definitions another integration recorded can be summarized: `hueTelemetry(hue)` with
|
|
377
626
|
`captureContent: false` records none, whereas an application whose AI SDK integration records
|
|
378
627
|
inputs and exports through Hue's attached processors gets the summary.
|
|
379
|
-
Recorded
|
|
380
|
-
|
|
381
|
-
and OpenAI `content` arrays. Supported byte shapes include GenAI `blob`, AI SDK `file` and
|
|
382
|
-
`image`, OpenAI Chat and Responses image, file and audio parts, Anthropic base64 sources, Gemini
|
|
383
|
-
`inlineData` and `inline_data`, Vertex `media`, and Bedrock image and document sources.
|
|
384
|
-
Remote URLs, provider file IDs and S3 references stay unchanged. Hue never fetches them.
|
|
385
|
-
|
|
386
|
-
Nonempty binary media in a span's own message attributes is eligible for upload at every size
|
|
387
|
-
within the inspection limits. Empty byte fields stay recorded and do not upload. The byte field is
|
|
388
|
-
removed and the part carries `sha256` and `size`; the span's `hue.blobs` entry points to the part,
|
|
389
|
-
for example `gen_ai.input.messages#/0/parts/1`. Text media over 64 KiB of decoded bytes uploads
|
|
390
|
-
with a UTF-8-safe 16 KiB prefix in its content field. Its entry points to that field, for example
|
|
391
|
-
`gen_ai.input.messages#/0/parts/1/content`. Smaller text stays inline. Hue reads both these forms
|
|
392
|
-
and earlier binary prefixes. If upload fails, the part keeps its digest with no blob entry.
|
|
393
|
-
In resources, events, links and log records, inline media up to 64 KiB stays verbatim; larger
|
|
394
|
-
inline media is replaced by its digest, as events, links and log records were in 0.16. These
|
|
395
|
-
nested values never upload. Ordinary hold, cut or drop rules still apply to the resulting record.
|
|
396
|
-
|
|
397
|
-
Entries use the recorded media type and, when valid, the filename, name or title. A valid name
|
|
398
|
-
is nonblank after trimming, at most 256 UTF-16 code units and well-formed Unicode, with no C0
|
|
399
|
-
characters (U+0000–U+001F) or DEL (U+007F). Invalid names are omitted
|
|
400
|
-
from the entry without rewriting the original field. Data URLs and fields explicitly encoded as
|
|
401
|
-
base64 by the provider contract are decoded before hashing. Other non-data strings with a valid
|
|
402
|
-
declared textual media type remain raw UTF-8: `YWJj` hashes as those four characters. The digest
|
|
403
|
-
matches `hue.recordFile`'s digest for the same bytes. Inspection is bounded to 8 Mi UTF-16
|
|
404
|
-
code units of text per message attribute and 16 Mi per record. Longer attributes use the ordinary value limit.
|
|
405
|
-
With `captureContent: false`, messages are removed without inspection or uploads. See
|
|
406
|
-
[Values over the inline limit](#values-over-the-inline-limit).
|
|
628
|
+
Recorded content can carry images, documents and audio: see
|
|
629
|
+
[Images, documents and audio](#images-documents-and-audio) for what the SDK uploads and how.
|
|
407
630
|
|
|
408
631
|
Manual helpers encode JSON values without converting null into absence. Unknown
|
|
409
632
|
outputs and usage remain absent. This SDK does not estimate tokens or cost. A thrown
|
|
@@ -543,8 +766,9 @@ dropped event and link counts, rather than being dropped whole.
|
|
|
543
766
|
|
|
544
767
|
Hue keeps a value inline up to its value limit (1 MiB unless the receiver advertises another in
|
|
545
768
|
`Hue-Max-Value-Bytes`). A span's own string attribute larger than that (helper content such as
|
|
546
|
-
`input.value`, `output.value` and tool results, or another instrumentation's attribute), and
|
|
547
|
-
|
|
769
|
+
`input.value`, `output.value` and tool results, or another instrumentation's attribute), and the
|
|
770
|
+
files described in [Images, documents and audio](#images-documents-and-audio), are uploaded to Hue
|
|
771
|
+
apart from their record, up to Hue's upload limit of 1,000,000,000 bytes:
|
|
548
772
|
|
|
549
773
|
1. Export computes the SHA-256 of the value's bytes (a string as UTF-8, a file as its own bytes)
|
|
550
774
|
and reserves it with `POST /api/v1/otlp/blobs`, authenticated with the project key. Hue answers
|
|
@@ -556,7 +780,7 @@ up to Hue's upload limit of 1,000,000,000 bytes:
|
|
|
556
780
|
the upload is confirmed with `POST /api/v1/otlp/blobs/complete`. A PUT the store answers with
|
|
557
781
|
412 (the same value uploaded at the same time by another span) counts as stored.
|
|
558
782
|
3. Whole string values and large text media keep a UTF-8-safe 16 KiB prefix. Binary media keeps
|
|
559
|
-
its digest on the part. The
|
|
783
|
+
its digest on the part. The record lists stored values in `hue.blobs`, at most 64 compact JSON
|
|
560
784
|
strings with fields ordered `key`, `sha256`, `size`, `content_type` and optional `name`:
|
|
561
785
|
`{"key":"output.value","sha256":"<64 hex>","size":5242880,"content_type":"application/json"}`.
|
|
562
786
|
A whole value is listed under `hue.truncated` as well, since its inline text is a prefix.
|
|
@@ -575,8 +799,8 @@ media parts keep their digest without a blob entry; failed opt-in `recordFile` u
|
|
|
575
799
|
the file event. The explicit helper limit is 25 MiB; whole attributes retain their 1 GB limit. Each
|
|
576
800
|
export reports such values in a `warning` issue per cause, whose `count` is the values and
|
|
577
801
|
`traceIds` their traces, and counts them in `report.uploadFallbacks`; successful uploads are counted
|
|
578
|
-
in `report.uploadedValues`. Both counters include whole attributes, eligible media
|
|
579
|
-
`recordFile` uploads. A warning never makes `flush()` throw. After a transient failure, or
|
|
802
|
+
in `report.uploadedValues`. Both counters include whole attributes, eligible media, attachments
|
|
803
|
+
and opt-in `recordFile` uploads. A warning never makes `flush()` throw. After a transient failure, or
|
|
580
804
|
a refusal that is not about the value alone, uploads pause for 30 seconds, and a receiver without
|
|
581
805
|
the upload route (a 404 that is not Hue's, a 405 or 501, or a 200 that is no reservation) is not
|
|
582
806
|
asked again for 10 minutes, so an unavailable store or another OTLP receiver meets no retry storm.
|
|
@@ -592,8 +816,9 @@ is held while nothing else is. While values can be uploaded, helpers encode cont
|
|
|
592
816
|
upload limit and 1,048,576 values, rather than cutting it to the value limit as they record it.
|
|
593
817
|
|
|
594
818
|
Nothing is uploaded with `captureContent: false` or a setup credential (`hue_setup_…`), nor from
|
|
595
|
-
log records
|
|
596
|
-
|
|
819
|
+
links, resources, log records not correlated with a span or in-progress placeholders; a log record
|
|
820
|
+
uploads only its files and the attachments in its body (see
|
|
821
|
+
[Automatically captured media](#automatically-captured-media)). Span events upload only through explicit `recordFile({ upload: true, ... })`;
|
|
597
822
|
other event values are cut as before. `redact` and the credential scrub of
|
|
598
823
|
tool definitions and recorded requests run on the whole value before it is hashed, so a value they
|
|
599
824
|
rewrite is uploaded as rewritten.
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/** How deep in a value an attachment's part may sit and still be uploaded: Hue reads media parts
|
|
2
|
+
* within ten levels of the value's root. */
|
|
3
|
+
export declare const MAX_ATTACHMENT_DEPTH = 10;
|
|
4
|
+
/** Options for {@link Attachment}. Give exactly one of `data` and `path`. */
|
|
5
|
+
export interface AttachmentOptions {
|
|
6
|
+
/** The file's bytes (a `Buffer`, typed array or `ArrayBuffer`), or a string holding them as
|
|
7
|
+
* base64 or a `data:` URL. */
|
|
8
|
+
data?: ArrayBufferView | ArrayBuffer | string;
|
|
9
|
+
/** A file to read when the attachment is created (Node.js file system). */
|
|
10
|
+
path?: string;
|
|
11
|
+
/** The file's media type, such as `image/png` or `application/pdf`. */
|
|
12
|
+
contentType: string;
|
|
13
|
+
/** The file name, recorded when `captureContent` is true. */
|
|
14
|
+
name?: string;
|
|
15
|
+
}
|
|
16
|
+
/** What an attachment holds, read by the client and transport only. */
|
|
17
|
+
export interface AttachmentState {
|
|
18
|
+
/** The file's bytes; undefined for an attachment that could not be read or is invalid. */
|
|
19
|
+
bytes?: Uint8Array;
|
|
20
|
+
sha256?: string;
|
|
21
|
+
contentType: string;
|
|
22
|
+
/** The name, when it is a valid media name. */
|
|
23
|
+
name?: string;
|
|
24
|
+
/** A name was given and is not a valid media name: it is omitted and counted. */
|
|
25
|
+
invalidName: boolean;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* A file placed in a recorded value, the equivalent of Langfuse's `LangfuseMedia` and
|
|
29
|
+
* Braintrust's `Attachment`. Put it anywhere in a `withSpan()` input or output, in `model()`
|
|
30
|
+
* messages, in a tool's arguments or result, in `recordMessages()` messages or in a span
|
|
31
|
+
* attribute passed to `withSpan()`. Hue records it at that position as a GenAI `blob` part that
|
|
32
|
+
* names the file by digest (`{ type: "blob", mime_type, modality, sha256, size, name }`) and
|
|
33
|
+
* uploads its bytes during export, listed under the record's `hue.blobs`.
|
|
34
|
+
*
|
|
35
|
+
* The bytes are read and hashed when the attachment is created; `data` and `path` are limited to
|
|
36
|
+
* 25 MiB. Creating one never throws: an attachment without exactly one of `data` and `path`, with
|
|
37
|
+
* unreadable data, larger data or an invalid `contentType` records `null` where it is placed and
|
|
38
|
+
* counts an instrumentation failure; an invalid `name` alone is omitted and counted.
|
|
39
|
+
*/
|
|
40
|
+
export declare class Attachment {
|
|
41
|
+
/** The media type given. */
|
|
42
|
+
readonly contentType: string;
|
|
43
|
+
/** The name given, when it is a valid media name. */
|
|
44
|
+
readonly name: string | undefined;
|
|
45
|
+
/** Lowercase hex SHA-256 of the bytes; undefined for an invalid attachment. */
|
|
46
|
+
readonly sha256: string | undefined;
|
|
47
|
+
/** Size of the bytes; undefined for an invalid attachment. */
|
|
48
|
+
readonly size: number | undefined;
|
|
49
|
+
constructor(options: AttachmentOptions);
|
|
50
|
+
}
|
|
51
|
+
/** The modality a media type implies, as Hue reads a part's media type. */
|
|
52
|
+
export declare function modalityOf(contentType: string): string;
|
|
53
|
+
/** An attachment's digest-form GenAI part, the name only with content capture; undefined for an
|
|
54
|
+
* invalid attachment. */
|
|
55
|
+
export declare function attachmentPart(state: AttachmentState, captureContent: boolean): Record<string, unknown> | undefined;
|
|
56
|
+
/** One attachment as a value is encoded: its state, its part (undefined when invalid), and the
|
|
57
|
+
* JSON pointer and depth of its position. */
|
|
58
|
+
export interface PlacedAttachment {
|
|
59
|
+
state: AttachmentState;
|
|
60
|
+
part?: Record<string, unknown>;
|
|
61
|
+
pointer: string;
|
|
62
|
+
depth: number;
|
|
63
|
+
}
|
|
64
|
+
/** Whether a value holds an attachment anywhere, within the encoder's bounds. */
|
|
65
|
+
export declare function holdsAttachment(value: unknown): boolean;
|
|
66
|
+
/** A JSON pointer's reference token. */
|
|
67
|
+
export declare function pointerSegment(segment: string): string;
|
|
68
|
+
/** The digest parts in `value` (within {@link MAX_ATTACHMENT_DEPTH}) that are the `files`' own
|
|
69
|
+
* parts: an object with `type` `blob`, no `content`, and the file's `sha256`, `size` and
|
|
70
|
+
* `mime_type`. Each found part takes one file of that digest, in the value's order; a part's
|
|
71
|
+
* `name`, when valid, is the one it carries. */
|
|
72
|
+
export declare function locateParts<T extends {
|
|
73
|
+
digest: {
|
|
74
|
+
sha256: string;
|
|
75
|
+
size: number;
|
|
76
|
+
};
|
|
77
|
+
contentType: string;
|
|
78
|
+
}>(value: unknown, files: readonly T[]): {
|
|
79
|
+
file: T;
|
|
80
|
+
pointer: string;
|
|
81
|
+
name?: string;
|
|
82
|
+
}[];
|