> For clean Markdown of any page, append .md to the page URL. > For a complete documentation index, see https://www.comet.com/docs/opik/tracing/advanced/log_multimodal_traces/llms.txt. > For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://www.comet.com/_mcp/server. # Log media & attachments Opik supports multimodal traces allowing you to track not just the text input and output of your LLM, but also images, videos and audio and any other media. ![](/docs/opik/_fern-img/99d85955ae6b263e219b27d9c0a9dee39ded4bb713ca6da0cad52ca9a3b70de3.webp) ## Size limits To keep ingestion fast and reliable, Opik enforces size limits on **inline** content - what you put directly in a span/trace `input`, `output`, or `metadata`: * **Per field (\~20 MB):** keep each inline, non-media `input`/`output` field under \~20 MB - the same cap also applies to `input` and `output` combined. Recent Opik SDK versions truncate an oversized `input`/`output` client-side before sending (replacing it with a truncation marker and logging a warning), so individual spans and traces stay within the limit. `metadata` is **not** truncated - keep it small too, since it still counts toward the per-request limit below. * **Per request:** a single ingestion batch is capped at roughly **\~50 MB compressed** and **\~256 MB uncompressed**; larger requests are rejected with `413`. **Base64-encoded content is handled separately and is not bound by the two limits above.** When you embed a large base64 blob in a field - images, audio, video, PDFs, and other recognized formats such as JSON - the SDK automatically extracts anything larger than \~250 KB and uploads it as an attachment * a separate upload, not part of the JSON ingestion request - so a single embedded value can be up to **100 MB**. See [Embedded base64 size limits](#embedded-base64-size-limits) below. Attachments are the recommended way to keep large content without hitting these limits: instead of embedding a large payload inline in `input`/`output`, log it as an attachment (below). The full payload is stored via object storage while your traces stay lightweight. As a rule of thumb, log summaries, top-K results, or IDs inline and attach anything large. ## Logging Attachments In the Python SDK, you can use the `Attachment` type to add files to your traces. Attachements can be images, videos, audio files or any other file that you might want to log to Opik. Each attachment is made up of the following fields: * `data`: The path to the file, raw bytes, or a base64 encoded string of the file * `file_name`: Optional name for the attachment (required when using raw bytes without a file path) * `content_type`: The content type of the file formatted as a MIME type These attachements can then be logged to your traces and spans using The `opik_context.update_current_span` and `opik_context.update_current_trace` methods: ### Using file paths The most common way to log attachments is by providing a file path: ```python wordWrap from opik import opik_context, track, Attachment @track def my_llm_agent(input): # LLM chain code # ... # Update the trace with a file path opik_context.update_current_trace( attachments=[ Attachment( data="", content_type="image/png", ) ] ) return "World!" print(my_llm_agent("Hello!")) ``` ### Using raw bytes (file-like data) You can also pass raw bytes directly to an attachment. This is useful when you have file content in memory (e.g., from an API response, generated content, or streaming data) and don't want to write it to disk first: ```python wordWrap from opik import opik_context, track, Attachment @track def process_image(image_bytes: bytes): # Process the image # ... # Log the raw bytes as an attachment opik_context.update_current_trace( attachments=[ Attachment( data=image_bytes, # Raw bytes file_name="processed_image.png", # Required for bytes content_type="image/png", ) ] ) return "Image processed!" # Example: Reading a file into memory and logging it with open("image.png", "rb") as f: image_data = f.read() print(process_image(image_data)) ``` > **Note** > > When using raw bytes, Opik automatically creates a temporary file for upload > and cleans it up after the attachment is uploaded. If you don't specify a > `content_type`, Opik will try to infer it from the `file_name` or default > to `application/octet-stream`. ### Logging images from HTTP responses A common use case is logging images fetched from external APIs or URLs: ```python wordWrap import httpx from opik import opik_context, track, Attachment @track def analyze_remote_image(image_url: str): # Fetch image from URL response = httpx.get(image_url) image_bytes = response.content content_type = response.headers.get("content-type", "image/jpeg") # Log the fetched image as an attachment opik_context.update_current_trace( attachments=[ Attachment( data=image_bytes, file_name="remote_image.jpg", content_type=content_type, ) ] ) # Process the image... return "Image analyzed!" # Analyze an image from a URL result = analyze_remote_image("https://example.com/image.jpg") ``` ### Logging generated content You can also log dynamically generated content like charts or reports: ```python wordWrap from opik import opik_context, track, Attachment import json @track def generate_report(data: dict): # Generate a JSON report report_bytes = json.dumps(data, indent=2).encode("utf-8") opik_context.update_current_trace( attachments=[ Attachment( data=report_bytes, file_name="report.json", content_type="application/json", ) ] ) return "Report generated!" ``` ### Using the Opik client directly You can also log attachments using the Opik client directly with both file paths and raw bytes: ```python wordWrap import opik from opik import Attachment client = opik.Opik() # Create a trace trace = client.trace( name="my-trace", input={"query": "Process this data"}, project_name="my-project", ) # Log attachment with file path span_with_file = client.span( trace_id=trace.id, name="file-attachment-span", attachments=[ Attachment( data="/path/to/document.pdf", content_type="application/pdf", ) ], ) # Log attachment with raw bytes binary_data = b"Hello, this is binary content!" span_with_bytes = client.span( trace_id=trace.id, name="bytes-attachment-span", attachments=[ Attachment( data=binary_data, file_name="data.bin", content_type="application/octet-stream", ) ], ) client.flush() ``` The attachements will be uploaded to the Opik platform and can be both previewed and dowloaded from the UI. ![](/docs/opik/_fern-img/99d85955ae6b263e219b27d9c0a9dee39ded4bb713ca6da0cad52ca9a3b70de3.webp) > **Note** > > In order to preview the attachements in the UI, you will need to supply a > supported content type for the attachment. We support the following content types: > > * Image: `image/jpeg`, `image/png`, `image/gif` and `image/svg+xml` > * Video: `video/mp4` and `video/webm` > * Audio: `audio/wav`, `audio/vorbis` and `audio/x-wav` > * Text: `text/plain` and `text/markdown` > * PDF: `application/pdf` > * Other: `application/json` and `application/octet-stream` ## Managing Attachments Programmatically You can also manage attachments programmatically using the [`AttachmentClient`](https://www.comet.com/docs/opik/python-sdk-reference/Objects/AttachmentClient.html): ```python wordWrap import opik opik_client = opik.Opik() attachment_client = opik_client.get_attachment_client() # Get list of attachments attachments_details = attachment_client.get_attachment_list( project_name="my-project", entity_id="some-trace-uuid-7", entity_type="trace" ) # Download an attachment attachment_data = attachment_client.download_attachment( project_name="my-project", entity_type="trace", entity_id="some-trace-uuid-7", file_name="report.pdf", mime_type="application/pdf" ) # Upload a new attachment attachment_client.upload_attachment( project_name="my-project", entity_type="trace", entity_id="some-trace-uuid-7", file_path="/path/to/document.pdf" ) ``` ## Previewing base64 encoded images and image URLs Opik automatically detects base64 encoded images and URLs logged to the platform, once an image is detected we will hide the string to make the content more readable and display the image in the UI. This is supported in the tracing view, datasets view and experiment view. For example if you are using the OpenAI SDK, if you pass an image to the model as a URL, Opik will automatically detect it and display the image in the UI: ```python wordWrap from opik.integrations.openai import track_openai from openai import OpenAI # Make sure to wrap the OpenAI client to enable Opik tracing client = track_openai(OpenAI()) response = client.chat.completions.create( model="gpt-4o-mini", messages=[ { "role": "user", "content": [ {"type": "text", "text": "What's in this image?"}, { "type": "image_url", "image_url": { "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg", }, }, ], } ], max_tokens=300, ) print(response.choices[0]) ``` ![](/docs/opik/_fern-img/b28450e12ff7f45f2e58ce32b39edb07a359baf00ec7eb23dbcd8edd0a5e824e.webp) ## Embedded Attachments When you embed base64-encoded media directly in your trace/span `input`, `output`, or `metadata` fields, Opik automatically optimizes storage and retrieval for performance. ### How It Works For base64-encoded content larger than 250KB, Opik automatically extracts and stores it separately. This happens transparently - you don't need to change your code. When you retrieve your traces or spans later, the attachments are automatically included by default. For faster queries when you don't need the attachment data, use the `strip_attachments=true` parameter. ### Embedded base64 size limits Opik Cloud supports embedded attachments up to **100MB per field**. This limit applies to individual base64 string values in your `input`, `output`, or `metadata` fields, and is separate from the [inline field/request limits](#size-limits) above - because base64 media above \~250KB is extracted and uploaded as an attachment rather than sent inline. > **Note** > > Base64 encoding increases file size by about 33%. For example, a 75MB video becomes \~100MB when base64-encoded. If you need to work with larger files: 1. **Use the Attachment API** - Upload files separately using `AttachmentClient` (recommended for files >50MB). See [Managing Attachments Programmatically](#managing-attachments-programmatically) 2. **Contact us** - [Get in touch](https://www.comet.com/site/about-us/contact-us/) if you need higher limits 3. **Self-host Opik** - Configure your own limits. See the [Self-hosting Guide](/self-host/overview) ### Best Practices * Embed smaller files directly - Opik handles them efficiently * For files >50MB, use the Attachment API for better performance * Use `strip_attachments=true` when querying if you don't need the attachment data ## Downloading attachments You can download attachments in two ways: 1. **From the UI**: Hover over the attachments and click on the download icon 2. **Programmatically**: Use the `AttachmentClient` as shown in the examples above > **Tip** > > Let's us know on [Github](https://github.com/comet-ml/opik/issues/new/choose) if you would like to us to support > additional image formats. > Debug, evaluate, and monitor your LLM applications, RAG systems, and agentic workflows with comprehensive tracing, automated evaluations, and production-ready dashboards.