@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/README.md
CHANGED
|
@@ -202,7 +202,179 @@ 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. A text
|
|
278
|
+
file a GenAI `blob`, Vertex `media` or AI SDK part holds as bare base64 takes the digest form.
|
|
279
|
+
|
|
280
|
+
Only the file's own value, or the members of the part that held it, change: every other byte of the
|
|
281
|
+
recorded text, its key order, number spellings, escapes and spacing included, is kept, in an encoded
|
|
282
|
+
string as in the attribute itself, so both SDKs write the same bytes. A record has 64 `hue.blobs`
|
|
283
|
+
places: the media inline in its values take them first, in reading order, then its attachments and
|
|
284
|
+
the files recorded with `upload`.
|
|
285
|
+
|
|
286
|
+
Entries use the recorded media type and, when valid, the filename, name or title. A valid name
|
|
287
|
+
is nonblank after trimming, at most 256 UTF-16 code units and well-formed Unicode, with no C0
|
|
288
|
+
characters (U+0000–U+001F) or DEL (U+007F). Invalid names are omitted from the entry without
|
|
289
|
+
rewriting the original field. Data URLs and fields explicitly encoded as base64 by the provider
|
|
290
|
+
contract are decoded before hashing. Other content is read as Hue's reader reads it, whatever its
|
|
291
|
+
media type: content that reads as base64 (a length divisible by four, base64 characters only, as
|
|
292
|
+
the GenAI conventions and `client.media.resolve` write a `blob` or `media` part's content) is the
|
|
293
|
+
file's base64 and is decoded, so `YWJj` hashes as `abc`; anything else is the file's own UTF-8
|
|
294
|
+
text. The digest matches `hue.recordFile`'s digest for the same bytes.
|
|
295
|
+
|
|
296
|
+
A file that is not uploaded (the upload failed, the receiver has no upload route, or a budget ran
|
|
297
|
+
out) keeps its digest without an entry in the recorded messages (`gen_ai.input.messages`,
|
|
298
|
+
`gen_ai.output.messages`, `ai.prompt.messages`); in every other attribute it keeps its bytes
|
|
299
|
+
inline under the ordinary value limit.
|
|
300
|
+
|
|
301
|
+
Log records upload their media the same way when they are correlated with a span:
|
|
302
|
+
`recordMessages`'s `gen_ai.client.inference.operation.details` record and any `gen_ai.*` event
|
|
303
|
+
with a structured body. The bytes upload under the log record's own trace and the log record lists
|
|
304
|
+
them in its own `hue.blobs`, keyed `body#<JSON pointer>` into the body
|
|
305
|
+
(`body#/gen_ai.input.messages/0/parts/1`, or `body#/content/1` for a deprecated
|
|
306
|
+
`gen_ai.user.message` event) or `<attribute>#<JSON pointer>` for the attributes above. One step
|
|
307
|
+
places a log record's files, the attachments in its body included. A body recorded as JSON text
|
|
308
|
+
stays as it is, and no log value is uploaded whole. A log file that is not uploaded is placed as
|
|
309
|
+
log records were before they uploaded: a file over 64 KiB in a recorded message attribute becomes
|
|
310
|
+
its digest and any other file keeps its bytes; text still over the value limit is cut inline and
|
|
311
|
+
listed under `hue.truncated`. While uploads are available, `recordMessages` records a body over
|
|
312
|
+
the value limit when it fits the limit with its files as their digest.
|
|
313
|
+
|
|
314
|
+
Inspection is bounded to 8 Mi UTF-16 code units of text per attribute and 16 Mi per record, a log
|
|
315
|
+
body's file contents included; a longer value uses the ordinary value limit. Inspection parses the
|
|
316
|
+
value and decodes each file while the record is queued and again when it is exported, holding
|
|
317
|
+
several copies of the value at once; larger files belong in an `Attachment` or in
|
|
318
|
+
`hue.recordFile` with `upload` set, whose bytes are not parsed out of text. Resources, links and
|
|
319
|
+
span events never upload inline media: in their recorded messages, media up to 64 KiB stays
|
|
320
|
+
verbatim and larger media is replaced by its digest, as in 0.16, and log records that cannot
|
|
321
|
+
upload (not correlated with a span, or while uploads are unavailable) keep the same rule. With
|
|
322
|
+
`captureContent: false`, nothing is inspected or uploaded.
|
|
323
|
+
|
|
324
|
+
### Attach a file anywhere
|
|
325
|
+
|
|
326
|
+
Wrap a file in an `Attachment` and put it anywhere in a value Hue records: a `withSpan()` input or
|
|
327
|
+
output, `model()` messages, a tool's arguments or result, `recordMessages()` messages or a span
|
|
328
|
+
attribute passed to `withSpan()`.
|
|
329
|
+
|
|
330
|
+
```ts
|
|
331
|
+
import { readFile } from "node:fs/promises";
|
|
332
|
+
import { Attachment } from "@hue-run/sdk";
|
|
333
|
+
|
|
334
|
+
const png = await readFile("scan.png");
|
|
335
|
+
await hue.withSpan(
|
|
336
|
+
"review contract",
|
|
337
|
+
(span) => {
|
|
338
|
+
span.setInput({
|
|
339
|
+
question: "Which clauses changed?",
|
|
340
|
+
contract: new Attachment({ path: "contract.pdf", contentType: "application/pdf" }),
|
|
341
|
+
});
|
|
342
|
+
},
|
|
343
|
+
{ attributes: { metadata: { scan: new Attachment({ data: png, contentType: "image/png" }) } } },
|
|
344
|
+
);
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
Give exactly one of `data` (a `Buffer`, typed array, `ArrayBuffer`, or a base64 string or `data:`
|
|
348
|
+
URL) and `path` (read from the file system when the attachment is created), a `contentType` and
|
|
349
|
+
optionally a `name`. The bytes are read and hashed when the attachment is created and are limited
|
|
350
|
+
to 25 MiB, the `recordFile` limit: they are held in memory from then until export, within the
|
|
351
|
+
128 MiB budget that queued spans share for values awaiting upload.
|
|
352
|
+
|
|
353
|
+
At its position the attachment is recorded as a GenAI `blob` part that names the file by digest,
|
|
354
|
+
`{"type":"blob","mime_type":"application/pdf","modality":"document","sha256":"<64 hex>","size":48213,"name":"contract.pdf"}`,
|
|
355
|
+
with `modality` read from the content type (`image`, `audio`, `video`, `document` or `file`).
|
|
356
|
+
Export uploads the bytes and lists them under the record's `hue.blobs`, keyed
|
|
357
|
+
`<attribute>#<JSON pointer to the part>`: `input.value#/contract` above, `metadata#/scan` for the
|
|
358
|
+
attribute, and `body#/gen_ai.input.messages/0/parts/1` for a part in a `recordMessages()` log
|
|
359
|
+
record, which uploads with the other media in the body. An attribute value that is an attachment,
|
|
360
|
+
a JSON object or an array OpenTelemetry cannot hold (not of one primitive type), with or without
|
|
361
|
+
attachments, is recorded as JSON text; put files under `metadata` for Hue to list them as the
|
|
362
|
+
span's metadata files.
|
|
363
|
+
|
|
364
|
+
With `captureContent: false` or a setup credential no bytes leave the process: a metadata
|
|
365
|
+
attribute still records the part, without its name, and content values are not recorded at all. A
|
|
366
|
+
part the redactor removes or rewrites is not uploaded. A part that cannot be uploaded keeps its
|
|
367
|
+
digest and is counted in `report.uploadFallbacks`, as is one nested more than 10 levels deep in its
|
|
368
|
+
value, where Hue does not read parts, or one in a value cut to the value limit. An attachment in a
|
|
369
|
+
value that is still over the value limit with its files as digest parts is uploaded and listed as
|
|
370
|
+
usual, and `client.media.fetch({ traceId, sha256 })` and Hue's span read reach it by digest; the
|
|
371
|
+
value itself uploads whole and keeps only its first 16 KiB inline, so readers do not place the file
|
|
372
|
+
at its position in the value. Creating an
|
|
373
|
+
attachment never throws: one without exactly one of `data` and `path`, with unreadable or larger
|
|
374
|
+
data or an invalid content type is recorded as `null` and counted as an instrumentation failure;
|
|
375
|
+
an invalid name alone is omitted and counted.
|
|
376
|
+
|
|
377
|
+
### Record a file
|
|
206
378
|
|
|
207
379
|
Record a file the work read, received or produced with `hue.recordFile`. It adds a `hue.file`
|
|
208
380
|
event to the active span, or to the span of an explicit context passed second. The event identifies
|
|
@@ -223,7 +395,9 @@ await hue.withSpan("review contract", async () => {
|
|
|
223
395
|
`data` is limited to 25 MiB. Larger data is omitted and counted as an instrumentation failure;
|
|
224
396
|
pass `sha256` and `byteSize` when you already have them. Upload requires supplied bytes, content
|
|
225
397
|
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.
|
|
398
|
+
same span, keyed `event:hue.file:<sha256>`, while preserving the event. An export fits every
|
|
399
|
+
span's events and entries before it uploads any of these files, then uploads the files of all its
|
|
400
|
+
spans together, four at a time; a file whose event was redacted or shed is not uploaded.
|
|
227
401
|
|
|
228
402
|
The event carries `hue.file.sha256`, `hue.file.role` (`input`, `attachment` or `output`),
|
|
229
403
|
`hue.file.media_type` and `hue.file.size` in both capture modes. `hue.file.name` requires
|
|
@@ -233,6 +407,118 @@ Tabs, newlines, DEL and lone surrogates make a name invalid. An invalid name is
|
|
|
233
407
|
counted as an instrumentation failure; the otherwise valid event remains. Invalid records and
|
|
234
408
|
records without an active span count as instrumentation failures.
|
|
235
409
|
|
|
410
|
+
### Read recorded media back
|
|
411
|
+
|
|
412
|
+
The evaluation client's `media` reads a recorded file back by its digest, with a key that may read
|
|
413
|
+
recorded content (**Read** or **Read and write**; a trace snapshot's copy needs **Read and write**):
|
|
414
|
+
|
|
415
|
+
```ts
|
|
416
|
+
import { createEvaluationClient } from "@hue-run/sdk/evals";
|
|
417
|
+
|
|
418
|
+
const client = createEvaluationClient({ apiKey: process.env.HUE_API_KEY! });
|
|
419
|
+
|
|
420
|
+
// One file by digest; pass the span that recorded it when you know it.
|
|
421
|
+
const file = await client.media.fetch({ traceId, spanId, sha256 });
|
|
422
|
+
console.log(file.contentType, file.fileName, file.size, file.bytes);
|
|
423
|
+
|
|
424
|
+
// Every digest part of a recorded value, written back in its own field.
|
|
425
|
+
const recorded = JSON.parse(attributes["gen_ai.input.messages"]);
|
|
426
|
+
const { value: messages, failures } = await client.media.resolve(recorded, { traceId, spanId });
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
`fetch` asks Hue for a five-minute download link (`GET /api/v1/traces/{traceId}/blobs/{sha256}`),
|
|
430
|
+
downloads it without the project key and refuses bytes whose SHA-256 is not the digest. It throws
|
|
431
|
+
`MediaFetchError`, whose `reason` is `not_found` (Hue holds no such file for the trace),
|
|
432
|
+
`missing` (the trace lists it, but it was never uploaded or has been erased), `unavailable` (still
|
|
433
|
+
unavailable after one retry that waits out a `Retry-After` of up to 10 seconds), `forbidden` (the
|
|
434
|
+
key cannot read recorded content, or reads a snapshot without **Read and write**), `too_large`
|
|
435
|
+
(over `maxBytes`, 64 MiB by default, checked before downloading), `digest_mismatch` or `failed` (a
|
|
436
|
+
connection failure, a timeout, a redirect or another unexpected answer). Hue is asked for the link
|
|
437
|
+
once more after a connection failure or a timeout, never after an unexpected answer; a download
|
|
438
|
+
ends once no byte has arrived for the client's timeout, at least two minutes, and is not retried.
|
|
439
|
+
`{ snapshotId, sha256 }` reads a trace snapshot's pinned copy instead
|
|
440
|
+
(`GET /api/v1/trace-snapshots/{snapshotId}/blobs/{sha256}`), which carries no media type or name.
|
|
441
|
+
|
|
442
|
+
`resolve` downloads each distinct digest once, four at a time (`concurrency`, up to 16), and returns
|
|
443
|
+
a new value; the input is not changed. With `as: "dataUrl"` (the default) each part receives a
|
|
444
|
+
`data:` URL where its field takes one (OpenAI `image_url` and `file_data`, Responses `input_image`,
|
|
445
|
+
`input_file` and `computer_screenshot`, AI SDK `image` and `file`) and bare base64 where its
|
|
446
|
+
convention requires it (GenAI `blob`, OpenAI `input_audio` and assistant `audio`, Anthropic
|
|
447
|
+
`source.data`, Gemini `inlineData`, Vertex `media`, Bedrock `source.bytes`), so the messages can be
|
|
448
|
+
sent to the provider again. A GenAI `blob` or Vertex `media` part receives base64 whatever its media
|
|
449
|
+
type, as the GenAI conventions define its content and as Hue reads content that reads as base64;
|
|
450
|
+
`as: "bytes"` writes a `Uint8Array`. A resolved part no longer carries `sha256` and `size`. A part
|
|
451
|
+
whose file cannot be read stays as it is and is listed in `failures` with its JSON pointer and
|
|
452
|
+
reason. Parts inside JSON-encoded strings, such as tool-call arguments, and Langfuse media tokens,
|
|
453
|
+
which name files Langfuse holds, are left as they are; `fetch` reads the former by the digest the
|
|
454
|
+
part carries.
|
|
455
|
+
|
|
456
|
+
A snapshot's copies carry no media type, so a part that declares none would get a `data:` URL of
|
|
457
|
+
`application/octet-stream`. Pass the record's `hue.blobs` entries in any form Hue reads (the JSON
|
|
458
|
+
strings the attribute holds, objects, or one string holding the JSON array), and such a part takes
|
|
459
|
+
the type of the entry with its SHA-256. Text over 64 KiB that a record keeps as its first 16 KiB
|
|
460
|
+
is never written back: a part whose content field an entry names, holding only that prefix, stays
|
|
461
|
+
as it is and is listed in `failures` as `prefix_only` with the entry's SHA-256, and
|
|
462
|
+
`fetch({ traceId, sha256 })` returns the whole file. An entry names a pointer, not a value, so the
|
|
463
|
+
entries of another attribute with a file at the same pointer are listed too; pass only the entries
|
|
464
|
+
of the value's own attribute to list its own file.
|
|
465
|
+
|
|
466
|
+
```ts
|
|
467
|
+
const { value } = await client.media.resolve(recorded, {
|
|
468
|
+
snapshotId,
|
|
469
|
+
blobs: attributes["hue.blobs"],
|
|
470
|
+
});
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
### Files in evaluation cases
|
|
474
|
+
|
|
475
|
+
`addCase` and `addEvalSetCase` pin up to 32 input files on a case with `files`: artifacts the
|
|
476
|
+
project already holds, or bytes and paths the client checks and uploads first. A case's target
|
|
477
|
+
receives its agent-visible files as `context.files`, and `rehydrateMedia(inputs, files)` writes
|
|
478
|
+
them back into the digest parts of recorded messages copied into the case, so the target can send
|
|
479
|
+
the recorded model call again with the real image, document or audio. See
|
|
480
|
+
[Case files](EVALUATIONS.md#case-files).
|
|
481
|
+
|
|
482
|
+
### Coming from Langfuse or Braintrust
|
|
483
|
+
|
|
484
|
+
| Langfuse | Braintrust | Hue |
|
|
485
|
+
| --- | --- | --- |
|
|
486
|
+
| Base64 data URIs in an observation's input, output or metadata | | [Automatic capture](#automatically-captured-media), for the attributes and shapes listed there |
|
|
487
|
+
| `LangfuseMedia` | `Attachment` | [`Attachment`](#attach-a-file-anywhere), at the same position |
|
|
488
|
+
| `resolveMediaReferences({ obj, resolveWith: "base64DataUri" })` | | `client.media.resolve(obj, { traceId })` |
|
|
489
|
+
| `GET /api/public/media/{mediaId}` | `ReadonlyAttachment.data()` | `client.media.fetch({ traceId, sha256 })` |
|
|
490
|
+
| Media tokens (`@@@langfuseMedia:…@@@`) | `ExternalAttachment` | Left as recorded: Hue never fetches a remote URL or a file another service holds. [`hue import langfuse --media`](#import-from-langfuse) copies Langfuse's files with your keys |
|
|
491
|
+
|
|
492
|
+
## Import from Langfuse
|
|
493
|
+
|
|
494
|
+
`hue import langfuse` copies a Langfuse project's history into Hue. It runs on your machine with
|
|
495
|
+
the Langfuse project's own keys (`LANGFUSE_PUBLIC_KEY`, `LANGFUSE_SECRET_KEY`, and
|
|
496
|
+
`LANGFUSE_BASE_URL` or `LANGFUSE_HOST`), which go only to Langfuse, and sends the spans with your
|
|
497
|
+
`HUE_API_KEY`:
|
|
498
|
+
|
|
499
|
+
```sh
|
|
500
|
+
npx -y @hue-run/sdk@0.18.1 import langfuse --env-path .env.langfuse --from 2026-01-01 --dry-run
|
|
501
|
+
npx -y @hue-run/sdk@0.18.1 import langfuse --env-path .env.langfuse --from 2026-01-01 --media
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
- It reads Langfuse Cloud and self-hosted v4 through the Observations API v2, and self-hosted v3
|
|
505
|
+
through the v1 traces and observations API, newest first; `--from`, `--to`, `--limit` and
|
|
506
|
+
`--trace-id` select traces, and `--dry-run` counts them without sending anything.
|
|
507
|
+
- Each observation becomes the span a Langfuse OpenTelemetry SDK records, with the
|
|
508
|
+
`langfuse.observation.*` and `langfuse.trace.*` attributes Hue reads, the user, session, release
|
|
509
|
+
and environment, and the model and token counts as GenAI attributes. Hex ids are kept and other
|
|
510
|
+
ids derived from them, so running the same command again resumes an import without duplicates.
|
|
511
|
+
- Spans are exported by the SDK's transport with its content-capture default; `--no-content`
|
|
512
|
+
sends metadata only.
|
|
513
|
+
- With `--media`, files that Langfuse media references point to are downloaded and uploaded under
|
|
514
|
+
the trace in the [digest form](#automatically-captured-media) the SDK writes, listed in
|
|
515
|
+
`hue.blobs`; a reference Langfuse holds no bytes for stays as recorded.
|
|
516
|
+
- Langfuse's rate limits are waited out for as long as `Retry-After` asks.
|
|
517
|
+
|
|
518
|
+
The command has no Python counterpart; it runs with `npx` for an application in any language. See
|
|
519
|
+
[Import traces from Langfuse](CLI.md#import-traces-from-langfuse) for every option, the attribute
|
|
520
|
+
mapping, media handling, limits and exit codes.
|
|
521
|
+
|
|
236
522
|
## Vercel AI SDK 6
|
|
237
523
|
|
|
238
524
|
AI SDK 6 accepts a per-call tracer through `experimental_telemetry`. Pass
|
|
@@ -376,34 +662,8 @@ definition list. The digest is the same in both SDKs and does not change when a
|
|
|
376
662
|
rotates. Only definitions another integration recorded can be summarized: `hueTelemetry(hue)` with
|
|
377
663
|
`captureContent: false` records none, whereas an application whose AI SDK integration records
|
|
378
664
|
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).
|
|
665
|
+
Recorded content can carry images, documents and audio: see
|
|
666
|
+
[Images, documents and audio](#images-documents-and-audio) for what the SDK uploads and how.
|
|
407
667
|
|
|
408
668
|
Manual helpers encode JSON values without converting null into absence. Unknown
|
|
409
669
|
outputs and usage remain absent. This SDK does not estimate tokens or cost. A thrown
|
|
@@ -543,8 +803,9 @@ dropped event and link counts, rather than being dropped whole.
|
|
|
543
803
|
|
|
544
804
|
Hue keeps a value inline up to its value limit (1 MiB unless the receiver advertises another in
|
|
545
805
|
`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
|
-
|
|
806
|
+
`input.value`, `output.value` and tool results, or another instrumentation's attribute), and the
|
|
807
|
+
files described in [Images, documents and audio](#images-documents-and-audio), are uploaded to Hue
|
|
808
|
+
apart from their record, up to Hue's upload limit of 1,000,000,000 bytes:
|
|
548
809
|
|
|
549
810
|
1. Export computes the SHA-256 of the value's bytes (a string as UTF-8, a file as its own bytes)
|
|
550
811
|
and reserves it with `POST /api/v1/otlp/blobs`, authenticated with the project key. Hue answers
|
|
@@ -556,7 +817,7 @@ up to Hue's upload limit of 1,000,000,000 bytes:
|
|
|
556
817
|
the upload is confirmed with `POST /api/v1/otlp/blobs/complete`. A PUT the store answers with
|
|
557
818
|
412 (the same value uploaded at the same time by another span) counts as stored.
|
|
558
819
|
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
|
|
820
|
+
its digest on the part. The record lists stored values in `hue.blobs`, at most 64 compact JSON
|
|
560
821
|
strings with fields ordered `key`, `sha256`, `size`, `content_type` and optional `name`:
|
|
561
822
|
`{"key":"output.value","sha256":"<64 hex>","size":5242880,"content_type":"application/json"}`.
|
|
562
823
|
A whole value is listed under `hue.truncated` as well, since its inline text is a prefix.
|
|
@@ -575,8 +836,8 @@ media parts keep their digest without a blob entry; failed opt-in `recordFile` u
|
|
|
575
836
|
the file event. The explicit helper limit is 25 MiB; whole attributes retain their 1 GB limit. Each
|
|
576
837
|
export reports such values in a `warning` issue per cause, whose `count` is the values and
|
|
577
838
|
`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
|
|
839
|
+
in `report.uploadedValues`. Both counters include whole attributes, eligible media, attachments
|
|
840
|
+
and opt-in `recordFile` uploads. A warning never makes `flush()` throw. After a transient failure, or
|
|
580
841
|
a refusal that is not about the value alone, uploads pause for 30 seconds, and a receiver without
|
|
581
842
|
the upload route (a 404 that is not Hue's, a 405 or 501, or a 200 that is no reservation) is not
|
|
582
843
|
asked again for 10 minutes, so an unavailable store or another OTLP receiver meets no retry storm.
|
|
@@ -592,8 +853,9 @@ is held while nothing else is. While values can be uploaded, helpers encode cont
|
|
|
592
853
|
upload limit and 1,048,576 values, rather than cutting it to the value limit as they record it.
|
|
593
854
|
|
|
594
855
|
Nothing is uploaded with `captureContent: false` or a setup credential (`hue_setup_…`), nor from
|
|
595
|
-
log records
|
|
596
|
-
|
|
856
|
+
links, resources, log records not correlated with a span or in-progress placeholders; a log record
|
|
857
|
+
uploads only its files and the attachments in its body (see
|
|
858
|
+
[Automatically captured media](#automatically-captured-media)). Span events upload only through explicit `recordFile({ upload: true, ... })`;
|
|
597
859
|
other event values are cut as before. `redact` and the credential scrub of
|
|
598
860
|
tool definitions and recorded requests run on the whole value before it is hashed, so a value they
|
|
599
861
|
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
|
+
}[];
|