@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/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
- ## Files
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 messages can contain images, documents and audio. Hue inspects
380
- `gen_ai.input.messages`, `gen_ai.output.messages` and `ai.prompt.messages`, including `parts`
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 eligible media in a span's own recorded messages, are uploaded to Hue apart from the span,
547
- up to Hue's upload limit of 1,000,000,000 bytes:
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 span lists stored values in `hue.blobs`, at most 64 compact JSON
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 and opt-in
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 (Hue does not read a log record's uploaded values yet), links, resources
596
- or in-progress placeholders. Span events upload only through explicit `recordFile({ upload: true, ... })`;
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
+ }[];