Skip to content

Retrieving Output Files

When a hosted tool produces a file — a matplotlib chart, an exported CSV — it surfaces on response.files as a FileOutput. This guide shows how to turn that descriptor into actual bytes plus the metadata (name, MIME type, size) you need to display or store it as an attachment — either buffered into a Blob or streamed straight to a sink.

response.files gives you a descriptor, not the bytes. Depending on the provider it carries an inline base64 data, a url, or a provider file id — three different retrieval mechanics. retrieveFile / streamFile collapse all three into one call, bound to the same model + key the completion already used (no re-passing credentials), and read the real name / type / size from the download response.

Both live on every CompleteResult, on LLMClient, and on AgentLoop (so an agent run can fetch the files it produced with the same call):

retrieveFile(file) → { blob: Blob; name?; mimeType: string; size: number }
streamFile(file) → { stream: ReadableStream<Uint8Array>; name?; mimeType?; size? }

Use when the file is small enough to hold at once (charts, small CSVs). You get a Blob you can open, plus the attachment metadata:

import { complete } from '@combycode/llm-sdk';
const { response, retrieveFile } = await complete({
model: 'anthropic/claude-haiku-4.5',
apiKey: process.env.ANTHROPIC_API_KEY,
prompt: 'Plot y = x**2 for x in 1..5 with matplotlib, save a PNG, and return the file.',
tools: [{ type: 'code_interpreter' }],
maxTokens: 6000,
});
for (const file of response.files ?? []) {
const { blob, name, mimeType, size } = await retrieveFile(file);
// name → 'chart.png' mimeType → 'image/png' size → 43940 blob.type is set
// browser: const url = URL.createObjectURL(blob); // <img src={url}> / <a download={name} href={url}>
// Node: await Bun.write(name ?? 'out.bin', blob);
}

Streamed — pipe large files without buffering

Section titled “Streamed — pipe large files without buffering”

When a file could be large (a big dataset export), stream it straight to a file, MongoDB GridFS, or an HTTP response. Nothing is buffered; the queue slot is released as soon as the transfer starts. The metadata comes back alongside the stream (a ReadableStream carries only bytes):

import { Readable } from 'node:stream';
import { createWriteStream } from 'node:fs';
const { stream, name, mimeType, size } = await streamFile(file);
// Node file sink:
Readable.fromWeb(stream).pipe(createWriteStream(name ?? 'out.bin'));
// MongoDB GridFS:
Readable.fromWeb(stream).pipe(bucket.openUploadStream(name ?? 'out.bin', { contentType: mimeType }));
// HTTP response (e.g. an Express handler):
res.setHeader('Content-Type', mimeType ?? 'application/octet-stream');
if (name) res.setHeader('Content-Disposition', `attachment; filename="${name}"`);
Readable.fromWeb(stream).pipe(res);

retrieveFile / streamFile read the download’s response headers so you can show a proper attachment:

FieldSource
mimeTypeContent-Type (falls back to the filename extension, then application/octet-stream)
nameContent-Disposition (the provider’s real filename, e.g. chart.png; RFC 5987 filename* preferred), else the FileOutput.name
sizeblob.size (buffered) / Content-Length (streamed, when the provider sends it)

You never branch on the provider — the helpers do:

FileOutput carriesProvidersRetrieval
inline base64 dataGoogledecoded directly, no HTTP
urlOpenAI (code-interpreter images)fetched from that URL
id (+ ref.containerId)Anthropic, OpenAI (container files)fetched from the provider’s files / container-files endpoint

Security: the provider’s credentials are attached only when the request targets the provider’s own host — a url pointing elsewhere gets no auth header, so keys never leak to a third-party host.

stream() has the same file coverage as complete(): a { type: 'file', file } event fires as each file finalizes, and the files are also collected onto the streamed final response. Fetch the bytes exactly the same way — see Hosted Code Execution.