hue.blobs attribute. Open the span in Traces to see the files: a file inside a message appears in place, other files appear below the messages, and values uploaded whole appear under Large values.
Uploads need content capture and an ordinary project key. With metadata-only capture or a setup credential (hue_setup_…), the SDKs upload nothing. Your redaction callback runs first: nothing it removes is uploaded, and a value it rewrites uploads as rewritten. A text pattern cannot see inside a file’s encoded bytes, so to keep personal data in documents out of Hue, drop or replace the file part in the callback. See Data privacy.
How files reach Hue
What the trace view shows
Raw PCM plays when its media type names its sample rate:
audio/L16;rate=24000 (big-endian, as RFC 2586 defines it) or audio/pcm;rate=16000;channels=1 (little-endian), with a whole rate from 8000 to 192000 and 1 or 2 channels. OpenAI audio declared pcm16 plays as 24 kHz mono. Raw PCM without a declared rate is download only. Playback depends on the browser’s support for the format.
A file listed in hue.blobs also shows a state: Stored, Not uploaded (not uploaded yet, never uploaded, or erased) or Unavailable (Hue could not read the storage).
Automatic capture
Hue recognizes files in these recorded forms, so images, audio and documents that OpenInference, OpenLLMetry or other instrumentation records inline appear without Hue code:- Message parts in the OpenTelemetry GenAI (
blob,uriandfile), Vercel AI SDK, OpenAI Chat Completions and Responses, Anthropic, Gemini, Vertex AI and Amazon Bedrock Converse formats, including OpenAI assistant audio and Responses computer screenshots. - MCP tool results’
imageandaudioblocks, and embeddedresourceblocks that hold bytes (blob), named by the last segment of the resource’s URI. A text resource stays text. - Pydantic AI
binaryparts,image-url,audio-url,video-urlanddocument-urlparts, and a tool’sBinaryContentreturn. - LangChain
image,audio,videoandfilecontent blocks withbase64data, aurlor a providerfile_id, in LangChain v1’s standard form or the earliersource_typeform. - OpenInference’s flattened image contents and OpenLLMetry’s JSON-encoded multipart prompts.
- A whole
data:URL that declares its media type, anywhere in recorded input, output, tool arguments, tool results or metadata, except as theuriof a GenAIuripart. Hue treats auripart as a reference and doesn’t read or upload adata:URL there; put inline bytes in ablobpart instead.
- Messages and values:
gen_ai.input.messages,gen_ai.output.messages,ai.prompt.messages,ai.prompt,input.valueandoutput.value(whatwithSpan/span()records),traceloop.entity.inputandtraceloop.entity.output, and Langfuse’slangfuse.observation.input,langfuse.observation.output,langfuse.trace.inputandlangfuse.trace.output. - Tool arguments and results:
gen_ai.tool.call.arguments,gen_ai.tool.call.result,ai.toolCall.args,ai.toolCall.result,tool.parameters,tool_argumentsandtool_response. - Metadata:
metadata,langfuse.observation.metadataandlangfuse.trace.metadata(whole, or one.<key>per attribute), andai.telemetry.metadata.<key>. - Indexed messages: OpenLLMetry’s
gen_ai.prompt.<n>.contentandgen_ai.completion.<n>.contentwhen the value is the JSON of a parts list, and OpenInference’s flattenedllm.input_messages.<n>.message.contents.<m>.message_content.image.image.urland itsllm.output_messagescounterpart.
Uint8Array in ai.prompt as an object of numbered bytes, which is not read as a file and uploads as a value many times the image’s size; pass image data to the AI SDK as a base64 string or data: URL instead.
Inside those values the SDKs read GenAI blob parts, AI SDK file and image parts, OpenAI Chat image, file and audio parts and an assistant message’s audio, Responses input_image, input_file and computer_screenshot, Anthropic base64 sources, Gemini inlineData and inline_data, Vertex media, Bedrock image and document sources, and any whole string that is a data: URL declaring a media type. Files inside a tool call’s JSON-encoded arguments and a tool result’s JSON string are found too.
Each uploaded file takes one of these forms; the rest of the recorded JSON text is kept byte for byte:
- A provider part loses its byte field and carries
sha256andsize, keyed at the part, for examplegen_ai.input.messages#/0/parts/1. An OpenAI assistant message’saudiolosesaudio.data, and the message carries the digest. - Raw 16-bit PCM keeps the format its type declares:
audio/pcm,audio/x-pcmoraudio/L16with arateis listed as, for example,audio/pcm;rate=24000;channels=1, and OpenAIpcm16audio asaudio/pcm;rate=24000;channels=1. - A whole
data:URL string becomes a GenAIblobpart,{"type":"blob","mime_type":"image/png","modality":"image","sha256":"<64 hex>","size":68}, keyed at the string. A value that is entirely one, such as the string input or output Langfuse’s SDK records as it is, becomes that part’s JSON text, keyed<attribute>#. A text data URL up to 64 KiB stays inline. - A file inside a JSON-encoded tool string takes the digest form inside the string. No JSON pointer can address inside a string, so Hue matches it by digest.
- Text media over 64 KiB of decoded bytes keeps a UTF-8-safe 16 KiB prefix in its content field, keyed at that field, for example
gen_ai.input.messages#/0/parts/1/content. A text file that a GenAIblob, Vertexmedia, or AI SDKfileorimagepart holds as bare base64 is decoded first: over 64 KiB of decoded bytes, it uploads and takes the digest form at the part, like binary media.
hue.blobs: its inline media first, in reading order, then its attachments and recorded files. An entry’s name comes from the part’s file name, name or title when it is valid: nonblank after trimming, at most 256 UTF-16 code units and well-formed Unicode, with no C0 control characters (U+0000–U+001F) or DEL (U+007F). Data URLs and base64 fields are decoded before hashing, so a part’s digest matches the digest of the same bytes recorded another way. Another string with a declared textual media type is hashed as UTF-8.
The SDKs leave a file as recorded when it is more than ten levels below a value’s root, when attribute JSON is nested past 32 levels or holds an integer past 2^53 (Hue keeps such JSON as text), and for an OpenInference input.value or output.value whose record declares it text/plain. A span’s own attribute too large to parse, such as a model call carrying minutes of base64 audio, is still read for files: each base64 or data: URL string in it that is longer than 16 Ki code units and written without escapes is decoded, hashed and uploaded a 1 MiB slice at a time, up to 1 GB, as long as the rest of the value’s JSON fits within 8 Mi code units.
Log records correlated with a span upload their media the same way: the inference-details record recordMessages() / log_inference() writes, and any gen_ai.* event with a structured body. The log record lists its files in its own hue.blobs, keyed body#<JSON pointer>, for example body#/gen_ai.input.messages/0/parts/1. A log record that cannot upload, such as one not correlated with a span, keeps the earlier rule: a file over 64 KiB in a recorded message becomes its digest, and other files keep their bytes. Resources, links and span events never upload; in their recorded messages, media up to 64 KiB stays as recorded and larger media becomes its digest.
Attach a file
Wrap a file in anAttachment and put it in any value Hue records: a span’s input or output, model messages, a tool’s arguments or result, inference-log messages, or a span attribute.
- TypeScript
- Python
data (bytes, or a base64 string or data: URL) and path, a content type and an optional name. data, limited to 25 MiB, and a file up to 25 MiB given by path are read and hashed when the attachment is created and stay in memory until they upload, within the 128 MiB the SDK holds for values awaiting upload. A larger file given by path, up to 1 GB, is hashed when the attachment is created and streamed from disk when it uploads. Keep that file in place and unchanged until the client flushes: a file that changed or is gone by then is not uploaded.
At its position the attachment is recorded as a GenAI blob part in digest form, {"type":"blob","mime_type":"application/pdf","modality":"document","sha256":"<64 hex>","size":48213,"name":"contract.pdf"}, keyed <attribute>#<JSON pointer to the part>: input.value#/contract and metadata#/scan above. Put files under metadata for Hue to list them as the span’s metadata files. A value still over the 1 MiB inline limit with its attachments as digest parts uploads whole and keeps only its first 16 KiB inline; the attachments’ bytes are stored and can be read back by digest, but are not placed at their positions in the value.
Creating an attachment never fails. One without exactly one of data and path, with unreadable or oversized data, or with an invalid content type is recorded as null (None in Python) and counted as an instrumentation failure. An invalid name alone is omitted and counted.
Record a file
To record a file your application reads or produces outside any value, such as the audio of a voice turn, record it on its span:- TypeScript
- Python
role is input, attachment or output. Give the file as data (bytes, up to 25 MiB, held in memory until it uploads) or path (a file on disk, up to 1 GB; over 25 MiB it streams from disk when it uploads). upload defaults to off: without it, the hue.file event records the digest, role, media type and size, but no bytes. Record audio in a format the trace view plays, such as WAV, MP3 or Opus; wrap raw PCM in a WAV header first.
Large files need time to upload. In TypeScript, each upload ends within six export timeouts plus 60 seconds of being queued: two minutes at the default timeoutMillis of 10,000, so a 1 GB file needs at least about 8.3 MB/s; raise timeoutMillis (up to 60,000) for slower links. In Python, each PUT attempt has six export timeouts or one second per MiB, whichever is longer.
Upload behavior
A span or log record is exported at once with its file references, and the bytes upload in the background, four at a time, so an upload never delays or drops a record. Each reservation andPUT (and, in Python, the completion) is tried up to three times on no answer, 408, 429 or 5xx, waiting Retry-After (up to 60 seconds) or a short backoff. A file used more than once in a trace is stored once.
- A file refused when its record is exported, because the receiver has no upload route, uploads were ended by shutdown or a budget ran out, keeps its digest without a
hue.blobsentry ingen_ai.input.messages,gen_ai.output.messagesandai.prompt.messages; in any other attribute it keeps its bytes inline, under the 1 MiB value limit. - A file whose upload fails after its record was exported keeps its digest and its entry, and shows Not uploaded until the same bytes are uploaded under the trace.
- A span’s own string attribute over the inline limit uploads whole, up to 1,000,000,000 bytes, and keeps a 16 KiB prefix inline.
- A file that cannot be uploaded never fails the export. It counts in
uploadFallbacksinhue.transport.getReport()(TypeScript) orhue.export_status.upload_fallbacks(Python); successful uploads count inuploadedValues/uploaded_values.
flush() and force_flush() send records first, then wait for the uploads queued before them, and a shutdown whose budget runs out ends the uploads still pending. In restricted-egress environments, allow HTTPS PUT to the upload URLs Hue returns.
Remote references
Hue stores only bytes that reach it. Neither the SDKs nor Hue fetch a remote URL, a provider file ID, an S3 reference or a file another service holds, so these stay as recorded with no bytes behind them. The trace view shows a web URL as a link, and a Langfuse media reference (@@@langfuseMedia:type=…|id=…|source=…@@@) with its media type as Held in Langfuse, with nothing to open or download. MCP get_span lists them with their uri, file_id or langfuse_media_id.
A case built from a trace has nothing to copy for these files, and judges cannot see them. To keep a file in Hue, record its bytes inline, as an attachment or with a recorded file.
With Langfuse’s TypeScript SDK and Hue’s processor beside Langfuse’s, Hue records the span before Langfuse replaces the file, so a data: URL or LangfuseMedia uploads to Hue in the background with no change to your code. Langfuse’s Python SDK replaces the file when the observation is set, so Hue’s copy holds the reference. For files already in Langfuse, run hue import langfuse --media with the Langfuse project’s keys.
Attach files without the SDK
An exporter that writes OTLP itself can record a small file inline, as adata: URL or base64 part, within the 1 MiB value limit. To attach a larger file, up to 1,000,000,000 bytes (1 GB), upload it apart from the span with the key that sends the trace (Tracing only in production):
- Reserve the upload with
POST https://app.hue.run/api/v1/otlp/blobsand a JSON body oftraceId(the span’s 32-hex trace ID),sha256(the lowercase hex SHA-256 of the bytes),byteSize,contentType(the media type) andkey(the file’shue.blobskey, below). If Hue already holds the bytes for this trace, it answers{"status": "exists", "ref": …}; go to step 4. Otherwise it answers{"status": "upload", "url": …, "method": "PUT", "headers": …, "expiresAt": …, "ref": …}. - Send the bytes with
PUTtourlwithin 15 minutes, with exactly the returnedheadersand noAuthorizationheader. A412answer means the bytes are already stored. - Complete the upload with
POST https://app.hue.run/api/v1/otlp/blobs/completeand a JSON body oftraceIdandsha256. Hue checks the stored bytes against the reservation. - Name the file on a span of that trace and list it in the span’s
hue.blobsattribute. The span can reach Hue before or after the upload, so you can export it first and upload the file afterwards. Retry a 429, a 5xx or a network failure a few times, never sooner than itsRetry-After. Hue shows the file once its bytes land.
- A
hue.fileevent, for a file the work read, received or produced:hue.file.sha256,hue.file.size,hue.file.media_type,hue.file.role(input,attachmentoroutput) and an optionalhue.file.name. Its key isevent:hue.file:<sha256>, and the file appears with the span’s other files. - A message part in digest form, for a file inside recorded messages: the part carries
sha256andsizein place of its content, for example{"type": "blob", "modality": "image", "mime_type": "image/png", "sha256": "<64 hex>", "size": 48213}. Its key is the attribute and the part’s JSON pointer, such asgen_ai.input.messages#/0/parts/1, and the file appears in place in the message.
hue.blobs is a string-array span attribute with one compact JSON entry per file: key, sha256, size, content_type and an optional name of 1 to 256 characters. The reservation’s ref is that entry without name. Hue reads up to 64 entries per span, one per key, and ignores an entry it cannot parse.
With the packages from the OpenTelemetry Python example, save this as attach.py next to a chart.png and run python attach.py:
Attach a voice turn
Attach a voice turn’s audio the same way: oneattach_file call per clip, with every entry in the span’s hue.blobs. Raw PCM, which realtime voice APIs stream, carries no header that names its sample rate: declare it in the media type, as above, or wrap it in a WAV header first. Python’s wave module does that without re-encoding. In attach.py, in place of the agent.request span:
Upload errors
The upload calls answer errors as JSON{"error": …}:
Read files back
An uploaded file is recorded content: it follows its trace’s retention, and reading it needs a Read or Read and write key, or a Hue MCP connection withinclude_content: true.
- TypeScript
- Python
fetch asks Hue for a five-minute download link, downloads it without the project key and refuses bytes whose SHA-256 is not the digest. A failure raises MediaFetchError with a reason: not_found, missing (listed but never uploaded, or erased), unavailable, forbidden (the key cannot read recorded content; a snapshot’s copy needs Read and write), too_large (over the size limit, 64 MiB by default), digest_mismatch or failed. A snapshot ID in place of the trace ID reads a snapshot’s pinned copy, which carries no media type or name. fetch also reads a file a producer recorded inline as a data: URL or base64 part of up to 1 MiB; pass the span ID for one in a log record.
resolve downloads each distinct digest once and returns a new value, leaving the input unchanged. By default each part gets a data: URL where its field takes one and bare base64 where its convention requires it, so the messages can be sent to the provider again; ask for bytes instead to write the bytes. A part whose file cannot be read stays as it is and is listed in failures with its JSON pointer and reason. Text kept as its first 16 KiB is never written back: pass the record’s hue.blobs entries as blobs, and such a part is listed as prefix_only, with the SHA-256 that fetch reads the whole file by. Parts inside JSON-encoded strings and Langfuse media references are left as they are. The TypeScript and Python references list every option.
Download over HTTP
GET https://app.hue.run/api/v1/traces/{traceId}/blobs/{sha256} returns a download link for a file of the trace: one that a span or correlated log record lists under hue.blobs, or one that a span or log record holds inline as a data: URL or base64 part of up to 1 MiB. Use a Read or Read and write key; any other key gets 403.
traceIdis Hue’s trace ID or the 32-hex OpenTelemetry trace ID.sha256is the file’s lowercase hex SHA-256, as MCPget_spanlists it underfiles.- The optional
spanIdquery parameter, a 16-hex OpenTelemetry span ID, reads only that span and its correlated log records, which is faster in a long trace. A file that a log record holds inline, or that a span holds in a format Hue began reading after the trace was recorded, is found only withspanId.
url is valid for five minutes and downloads the file as fileName. Request it without your Authorization header. An inline file downloads under its recorded name, or else one made from its role and digest, such as input-4bf92f3577b3.png.
Verify uploaded media
Delivery of a span does not prove its files arrived, because bytes upload after the record. Check media separately:- Flush with a budget that covers the uploads, then check
uploadFallbacks/upload_fallbacksis unchanged. - Read the span with MCP
get_span: each file infilesshould havestate: "stored".missingmeans the bytes never arrived. Withinclude_content: true, each file also carries a five-minutedownload_url; treat it as a credential and don’t paste it into chats or reports. - Download one file with
client.media.fetchor the HTTP endpoint and compare its SHA-256 and size with the bytes you sent.
Files in cases
A case keeps its own copies of the images, documents and audio its trace recorded, so they stay with the case after the trace is deleted. Hue copies up to 32 files of up to 25 MiB each. Create a case explains which files judges see. To pin input files on a case yourself, pass up to 32files to addCase or addEvalSetCase (add_case / add_eval_set_case with files= of CaseFileWrite in Python): name an artifact the project already holds, or give the file’s role, name, content type and bytes or path. Each file must be one of the supported content types, named with 1 to 255 UTF-16 code units and 1 byte to 25 MiB; the client uploads it as a verified artifact before the case and reuses it when you send the same case again. A file outside that policy, or a path that cannot be read or changed after the check, fails before the case is written.
During a run, the runner downloads the case’s files and checks each one’s size and SHA-256. The target receives the agent-visible files (source, attached_template, attached_reference and original) as context.files; org_template and evaluator_reference files reach only evaluators. rehydrateMedia(inputs, files) / rehydrate_media(inputs, files) writes the case’s files back into recorded messages by SHA-256, so a target can send a recorded model call again with the real files.