File upload + reference
What you will achieve
Section titled “What you will achieve”Pass a file path (or a Blob) as an attachment to complete(). The SDK uploads it to the provider’s file storage on first use, caches the remote id on the FileAttachment object, and sends a reference in subsequent calls — no repeated byte transfers. For providers without file storage, the file is inlined as base64 automatically.
When and why
Section titled “When and why”Provider file APIs let you upload once and reuse across many requests. The wins:
- Reduced token cost — file content bytes are not retransmitted per request; only a file id is sent.
- Large files — some providers accept files up to 2 GB via the Files API but cap inline base64 at a few MB.
- Multi-turn document Q&A — upload a PDF once, ask many questions without resending.
The raw problem: OpenAI requires a multipart files.create() call then { type: 'input_file', file_id } in the message content. Google uses uploadFile() and a fileUri. Anthropic has its own Files API endpoint with source: { type: 'file', file_id } in content blocks. Three upload methods, three reference shapes, three expiry policies.
Step by step
Section titled “Step by step”Step 1 — Attach a file by path
Section titled “Step 1 — Attach a file by path”The simplest usage — pass a file path string in attachments:
import { complete } from '@combycode/llm-sdk';
const { text } = await complete({ model: process.env.LLM_MODEL!, apiKey: process.env.LLM_API_KEY, prompt: 'What word is in this file? Reply with just the word.', attachments: ['./banana.txt'], maxTokens: 32,});
console.log(text.trim().toLowerCase()); // 'banana'The attachments array accepts:
- A string path (Node/Bun — resolved relative to cwd)
- A
Uint8Array(raw bytes — treated as image by MIME detection) - A
ContentPart(used as-is — full control)
Step 2 — Understand the decision flow
Section titled “Step 2 — Understand the decision flow”When a file attachment is resolved before the request is sent, the FilesRegistry applies a FileStrategy decision:
| Condition | Decision |
|---|---|
| Provider has no registered file adapter | inline (base64 in message body) |
| File already uploaded and not expired | Use cached provider_ref (the stored remote id) |
| File is expired | reupload (re-upload, get a new id) |
| File is a URL and provider supports direct URLs (OpenAI, xAI) | url (send the URL directly) |
| File is smaller than 50 KB | inline (base64 — skip the upload round-trip) |
| File is 50 KB or larger | upload (upload once, cache the remote id) |
| File exceeds provider max size | skip (replace with a text placeholder) |
| Provider does not support the MIME type | skip |
The inline threshold (50 KB) is the default. You can override it by passing a custom FileStrategy when building the engine.
Step 3 — Upload a Blob (browser or Node)
Section titled “Step 3 — Upload a Blob (browser or Node)”import { complete, FileAttachment } from '@combycode/llm-sdk';
// From a browser <input type="file">const input = document.querySelector('input[type="file"]') as HTMLInputElement;const blob = input.files![0];
const attachment = FileAttachment.fromBlob(blob);
const { text } = await complete({ model: 'openai/gpt-4o', prompt: 'Describe the contents of this file.', attachments: [attachment], // ContentPart-compatible maxTokens: 512,});FileAttachment.fromBlob(blob) reads blob.name, blob.type, and blob.size automatically.
Step 4 — Check upload state
Section titled “Step 4 — Check upload state”FileAttachment tracks upload state per provider via uploads: Map<string, FileUploadState>:
import { FileAttachment } from '@combycode/llm-sdk';
const file = new FileAttachment({ filename: 'report.pdf', mimeType: 'application/pdf', sizeBytes: 204800, content: { type: 'path', mimeType: 'application/pdf', path: './report.pdf' },});
// After complete() has run -- state is stored on the file object:console.log(file.isAvailable('openai')); // true after first callconsole.log(file.getRef('openai')); // 'file-abc123' (OpenAI remote id)
const state = file.uploads.get('openai');console.log(state?.status); // 'uploaded' | 'pending' | 'expired' | 'deleted' | 'error'console.log(state?.remoteId); // provider's file idconsole.log(state?.expiresAt); // Unix ms timestamp or nullStep 5 — Expiry and provider limits
Section titled “Step 5 — Expiry and provider limits”| Provider | Max file size | Expiry |
|---|---|---|
| OpenAI | 512 MB (Responses API) | Persistent until deleted |
| Anthropic | — (Files API) | 30 days |
| 2 GB (File API) | 48 hours | |
| xAI | — | Provider-dependent |
The strategy checks expiresAt against Date.now() before every request. If expired, the file is re-uploaded automatically. file.uploads.get(provider)?.status transitions from 'uploaded' to 'expired' when the TTL passes.
Step 6 — Use DataSource: 'file' in content parts (advanced)
Section titled “Step 6 — Use DataSource: 'file' in content parts (advanced)”When building messages manually, reference a FileAttachment via a file data source:
import { complete, FileAttachment } from '@combycode/llm-sdk';
const file = new FileAttachment({ filename: 'data.pdf', mimeType: 'application/pdf', sizeBytes: 10000, content: { type: 'path', mimeType: 'application/pdf', path: './data.pdf' },});
const { text } = await complete({ model: 'anthropic/claude-opus-4.8', prompt: [ { role: 'user', content: [ { type: 'document', source: { type: 'file', fileId: file.id }, }, { type: 'text', text: 'Summarise this document in two sentences.' }, ], }, ], maxTokens: 256,});The onMessageResolve hook fires before the request is sent and replaces the file source with either a provider_ref (uploaded) or base64 (inlined) source.
Your options
Section titled “Your options”attachments in CompleteOptions:
| Value type | Behaviour |
|---|---|
string (path) | File read from disk (Node/Bun). MIME type inferred from extension. |
string (http(s)://) | Treated as a URL source. Provider-specific: OpenAI/xAI send the URL directly; others fetch and inline. |
Uint8Array | Treated as image data; MIME type defaults to image/jpeg. |
ContentPart | Used as-is — full control over type and source. |
FileAttachment | Managed file — upload state tracked, remote id reused automatically. |
FileContent types (for FileAttachment constructor):
type | When to use |
|---|---|
'path' | Node/Bun — file on disk. Read lazily when needed. |
'buffer' | Raw Uint8Array bytes already in memory. |
'blob' | Browser Blob or File object. |
'base64' | Already-encoded base64 string. Skips the encoding step. |
'url' | Remote URL. Sent directly to providers that accept URLs; fetched + inlined for others. |
FileDecision actions (what the strategy decides):
| Action | Meaning |
|---|---|
'upload' | Upload the file, cache the remote id, send provider_ref. |
'reupload' | Previous upload expired; upload again, get a new id. |
'inline' | Encode as base64 and embed directly in the message body. |
'url' | Send the file URL directly (provider fetches it). |
'skip' | Provider does not support this file type or the file is too large; replaced with a text placeholder. |
Custom strategy:
import { DefaultFileStrategy, type FileStrategyContext, type FileDecision } from '@combycode/llm-sdk';
class AlwaysInlineStrategy extends DefaultFileStrategy { decide(ctx: FileStrategyContext): FileDecision { return { action: 'inline', reason: 'always inline for this engine' }; }}Pass the custom strategy when building a FilesRegistry or engine.
Compare the SDKs
Section titled “Compare the SDKs”OpenAI’s SDK requires calling files.create() with a ReadStream or File object, storing the returned file_id, then referencing it as { type: 'input_file', file_id } in message content — three separate steps per file per provider. Google and Anthropic each have their own upload methods and reference shapes. ORXA’s FilesRegistry handles upload, caching, expiry detection, and reference injection automatically. The FileAttachment object persists upload state across calls, so the same file object can be reused in a conversation loop without re-uploading.
Gotchas and next steps
Section titled “Gotchas and next steps”Files are tracked per FileAttachment instance, not by path. Two FileAttachment objects created from the same path are independent — each will upload separately. Reuse the same object to benefit from upload caching.
Small files (under 50 KB) are inlined, not uploaded. If you pass a 30 KB PDF, it will be base64-encoded in the request body on every call, not uploaded to the Files API. This is intentional (avoids upload latency for small files). Override the threshold with a custom strategy if needed.
Google file expiry is 48 hours. If you store a FileAttachment in a database and restore it after 48 hours, file.isAvailable('google') will return false (expired) and the SDK will re-upload automatically.
attachments sugar is for one-shot calls. For multi-turn conversations where the same file is referenced across many messages, build the FileAttachment explicitly and include it in the message content manually — this gives you full control over the upload lifecycle.
Next steps:
- PDF document input — PDF-specific handling and Anthropic document blocks
- Async batch — reference uploaded files in large-scale batch jobs
- Vision / image input — inline image attachments without upload