Skip to content

feat(ai): native Files API support across providers (upload adapters + file content source) #909

Description

@tombeckenham

Summary

Add first-class support for provider Files / storage APIs so callers can upload media once and reference it by a provider-issued handle, instead of re-sending base64 or relying on the provider to fetch a public URL every request.

Motivated by the discussion on #907 (Gemini image URL handling) and the original ask from @L4Ph about createGeminiImage() supporting the Files API.

Why

Today ContentPartSource is a two-arm union — { type: 'data' } (inline base64) or { type: 'url' } (HTTP(S)/gs:///data URI). That covers a lot, but it can't express a provider file handle, and it doesn't give us a place to hang native upload.

Benefits: large/reused inputs uploaded once (lower latency + bandwidth), no re-buffering of base64 on memory-constrained runtimes (the #907 OOM class), and access to provider-side file lifecycle (TTL, deletion).

Provider support (verified against the vendored SDKs in this repo)

Provider Native upload surface Handle referenced as Notes
Gemini ai.files.upload() fileData.fileUri (a generativelanguage.googleapis.com/... URL) Handle is a URL, so it already round-trips through { type: 'url' } today (image-gen fixed in #907).
OpenAI openai.files.upload() input_image / input_file file_id (file-...) ID is not a URL — no representation today.
Anthropic client.beta.files.upload() (beta) file_id message source ID is not a URL — no representation today.
fal fal.storage.upload() resulting storage URL fal-client already auto-uploads Blob/File inputs (see ai-fal/src/utils/client.ts); we convert data: URLs to Blobs so it can. Could be surfaced explicitly.
xAI / Grok (no dedicated files API found) image.url / images[].url Grok's image edits accept URLs directly (passthrough), so no upload step is needed for URL inputs.

Proposed shape

1. Tree-shakeable files adapter kind (following the existing Text/Embed/Image split), for providers that have one:

import { geminiFiles } from '@tanstack/ai-gemini/adapters'
const files = geminiFiles()
const handle = await files.upload({ data, mimeType }) // → { id, uri?, expiresAt? }
await files.get(handle.id)
await files.delete(handle.id)

openaiFiles(), anthropicFiles(), falFiles() (wrapping fal.storage.upload) likewise. Grok has nothing to wrap — URL inputs already work.

2. New { type: 'file' } arm on ContentPartSource:

{ type: 'image', source: { type: 'file', value: fileIdOrUri, provider: 'openai' } }

Per-provider typing should enforce that a file reference only flows to the provider that issued it (a file-... ID sent to Gemini is a bug). Adapters map it to the right wire field (file_id / fileData.fileUri).

3. Optional follow-up: an auto-upload helper that, above a size threshold, uploads a { type: 'data' } input via the provider's Files API and swaps in the handle — the large-input path #907 called out. fal already does a form of this internally.

Related: server-side URL buffering audit (#907 class)

Sweeping the repo for adapters that fetch + buffer image URL inputs locally:

  • Gemini image — was buffering; fixed in fix(ai-gemini): align createGeminiImage URL handling with chat — stop fetching arbitrary HTTPS URLs #907 (now fileData passthrough).
  • Gemini Veo (imagePartToVeoImage) — still buffers HTTP URLs, but required: the predict API's Image type only accepts imageBytes or gcsUri, no URI passthrough.
  • OpenAI image edits (imagePartToFile) — still buffers, but required: /v1/images/edits is multipart and needs real bytes; there's no "fetch this URL" option. A files.upload handle would be the memory-safe alternative here.
  • Everyone else passes URLs through without buffering: Gemini chat, fal image, Grok image, Mistral vision, OpenAI vision. (OpenAI/Grok/arrayBufferToBase64 hits elsewhere are all output handling — downloading generated media — not input buffering.)

Scope / open questions

  • Capability gating: which models/endpoints accept file handles varies (Imagen vs native Gemini image, Responses vs Chat Completions). Needs feature-support / model-meta wiring.
  • E2E: aimock coverage for upload endpoints (likely unit-first, mirroring the image-gen mapping note in feature-support.ts).
  • Not all provider file handles are cross-compatible with all generation types — the type layer should reflect that.

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions