Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions .changeset/openai-pdf-document-input.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
---
'@tanstack/openai-base': minor
'@tanstack/ai-openai': minor
---

feat(ai-openai): support PDF `document` content parts in the Responses adapter.

`openaiText`'s Responses adapter now accepts PDF `document` content parts, for any model whose `model-meta` entry declares the `document` input modality. Base64 data sources are sent as `input_file` with a `file_data` data URL and a `filename` (from `metadata.filename`, defaulting to `document.pdf`). URL sources are sent as `input_file` with `file_url`.

```ts
const adapter = openaiText('gpt-5.5')

const message = {
role: 'user',
content: [
{ type: 'text', content: 'Summarize this document' },
{
type: 'document',
source: { type: 'data', value: pdfBase64, mimeType: 'application/pdf' },
metadata: { filename: 'report.pdf' },
},
],
}
```

Non-PDF MIME types are rejected before the request is sent — including pre-wrapped `data:` URLs whose media type disagrees with `mimeType` — so callers get an actionable message instead of an opaque provider `400`. Inline base64 payloads are also checked for the `%PDF` magic bytes, so non-PDF data mislabeled (or sent without) a PDF `mimeType` is rejected locally. `OpenAIDocumentMetadata` gains `filename` and `detail`. The Chat Completions adapter throws a document-specific error pointing here, since documents are Responses-only.
27 changes: 24 additions & 3 deletions docs/advanced/multimodal-content.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,11 +94,12 @@ const response = await chat({

### OpenAI

OpenAI supports images and audio in their vision and audio models:
OpenAI supports images, audio, and PDF documents in their vision, audio, and
document-capable models:

```typescript
import { openaiText } from '@tanstack/ai-openai'
import { imageBase64 } from './data'
import { imageBase64, pdfBase64 } from './data'

const adapter = openaiText('gpt-5.5')

Expand All @@ -116,10 +117,30 @@ const message = {
}
```

```typescript
import { pdfBase64 } from './data'

// PDF document via base64 data (the API requires a filename alongside
// inline data; defaults to "document.pdf" when omitted)
const documentMessage = {
role: 'user',
content: [
{ type: 'text', content: 'Summarize this document' },
{
type: 'document',
source: { type: 'data', value: pdfBase64, mimeType: 'application/pdf' },
metadata: { filename: 'report.pdf' }
}
]
}
```

**Supported modalities by model:**
- `gpt-5.2`, `gpt-5-mini`: text, image
- `gpt-5.5`, `gpt-5.2`, `gpt-5-mini` (among others): text, image, PDF document
- `gpt-4o-audio`: text, audio

Check each model's `supports.input` in `@tanstack/ai-openai`'s `model-meta.ts` for the authoritative per-model list.

### Anthropic

Anthropic's Claude models support images and PDF documents:
Expand Down
3 changes: 2 additions & 1 deletion docs/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -477,7 +477,8 @@
{
"label": "Multimodal Content",
"to": "advanced/multimodal-content",
"addedAt": "2026-04-15"
"addedAt": "2026-04-15",
"updatedAt": "2026-07-21"
},
{
"label": "Per-Model Type Safety",
Expand Down
27 changes: 25 additions & 2 deletions packages/ai-openai/src/message-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,32 @@ export interface OpenAIVideoMetadata {}

/**
* Metadata for OpenAI document content parts.
* Note: Direct document support may vary; PDFs often need to be converted to images.
*
* Documents are supported by the Responses adapter only, which sends them
* as `input_file`; the Chat Completions adapter rejects document parts.
* This adapter currently supports only `application/pdf` documents. Inline
* (base64) data is verified locally — both the declared/embedded MIME type
* and the `%PDF` content header, so mislabeled non-PDF data is rejected
* before the request. URL-sourced documents are sent to OpenAI unvalidated,
* so a non-PDF URL fails server-side instead.
*
* @see https://developers.openai.com/api/docs/guides/pdf-files
*/
export interface OpenAIDocumentMetadata {}
export interface OpenAIDocumentMetadata {
/**
* Filename sent alongside inline (base64) PDF data.
* @default 'document.pdf'
*/
filename?: string
/**
* Rendering quality for the file's page images. Omitted by default so the
* API applies its own default ('auto'), which resolves to 'low' or 'high'
* depending on the model.
*
* @see https://developers.openai.com/api/docs/guides/pdf-files
*/
detail?: 'auto' | 'low' | 'high'
}

/**
* Metadata for OpenAI text content parts.
Expand Down
46 changes: 23 additions & 23 deletions packages/ai-openai/src/model-meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import type {
interface ModelMeta<TProviderOptions = unknown> {
name: string
supports: {
input: Array<'text' | 'image' | 'audio' | 'video'>
input: Array<'text' | 'image' | 'audio' | 'video' | 'document'>
output: Array<'text' | 'image' | 'audio' | 'video'>
endpoints: Array<
| 'chat'
Expand Down Expand Up @@ -74,7 +74,7 @@ const GPT5_2 = {
max_output_tokens: 128_000,
knowledge_cutoff: '2025-08-31',
supports: {
input: ['text', 'image'],
input: ['text', 'image', 'document'],
output: ['text'],
endpoints: ['chat', 'chat-completions'],
features: [
Expand Down Expand Up @@ -119,7 +119,7 @@ const GPT5_2_PRO = {
max_output_tokens: 128_000,
knowledge_cutoff: '2025-08-31',
supports: {
input: ['text', 'image'],
input: ['text', 'image', 'document'],
output: ['text'],
endpoints: ['chat', 'chat-completions'],
features: ['streaming', 'function_calling'],
Expand Down Expand Up @@ -158,7 +158,7 @@ const GPT5_2_CHAT = {
max_output_tokens: 16_384,
knowledge_cutoff: '2025-08-31',
supports: {
input: ['text', 'image'],
input: ['text', 'image', 'document'],
output: ['text'],
endpoints: ['chat', 'chat-completions'],
features: ['streaming', 'function_calling', 'structured_outputs'],
Expand Down Expand Up @@ -194,7 +194,7 @@ const GPT5_1 = {
max_output_tokens: 128_000,
knowledge_cutoff: '2024-09-30',
supports: {
input: ['text', 'image'],
input: ['text', 'image', 'document'],
output: ['text', 'image'],
endpoints: ['chat', 'chat-completions'],
features: [
Expand Down Expand Up @@ -240,7 +240,7 @@ const GPT5_1_CODEX = {
max_output_tokens: 128_000,
knowledge_cutoff: '2024-09-30',
supports: {
input: ['text', 'image'],
input: ['text', 'image', 'document'],
output: ['text', 'image'],
endpoints: ['chat'],
features: ['streaming', 'function_calling', 'structured_outputs'],
Expand Down Expand Up @@ -277,7 +277,7 @@ const GPT5 = {
max_output_tokens: 128_000,
knowledge_cutoff: '2024-09-30',
supports: {
input: ['text', 'image'],
input: ['text', 'image', 'document'],
output: ['text'],
endpoints: ['chat', 'chat-completions', 'batch'],
features: [
Expand Down Expand Up @@ -323,7 +323,7 @@ const GPT5_MINI = {
max_output_tokens: 128_000,
knowledge_cutoff: '2024-05-31',
supports: {
input: ['text', 'image'],
input: ['text', 'image', 'document'],
output: ['text'],
endpoints: ['chat', 'chat-completions', 'batch'],
features: ['streaming', 'structured_outputs', 'function_calling'],
Expand Down Expand Up @@ -373,7 +373,7 @@ const GPT5_NANO = {
},
},
supports: {
input: ['text', 'image'],
input: ['text', 'image', 'document'],
output: ['text'],
endpoints: ['chat', 'chat-completions', 'batch'],
features: ['streaming', 'structured_outputs', 'function_calling'],
Expand Down Expand Up @@ -413,7 +413,7 @@ const GPT5_PRO = {
},
},
supports: {
input: ['text', 'image'],
input: ['text', 'image', 'document'],
output: ['text'],
endpoints: ['chat', 'batch'],
features: ['streaming', 'structured_outputs', 'function_calling'],
Expand Down Expand Up @@ -454,7 +454,7 @@ const GPT5_CODEX = {
},
},
supports: {
input: ['text', 'image'],
input: ['text', 'image', 'document'],
output: ['text', 'image'],
endpoints: ['chat'],
features: ['streaming', 'structured_outputs', 'function_calling'],
Expand Down Expand Up @@ -686,7 +686,7 @@ const O3_PRO = {
},
},
supports: {
input: ['text', 'image'],
input: ['text', 'image', 'document'],
output: ['text'],
endpoints: ['chat', 'batch'],
features: ['function_calling', 'structured_outputs'],
Expand Down Expand Up @@ -839,7 +839,7 @@ const O3 = {
},
},
supports: {
input: ['text', 'image'],
input: ['text', 'image', 'document'],
output: ['text'],
endpoints: ['chat', 'batch', 'chat-completions'],
features: ['function_calling', 'structured_outputs', 'streaming'],
Expand Down Expand Up @@ -880,7 +880,7 @@ const O4_MINI = {
},
},
supports: {
input: ['text', 'image'],
input: ['text', 'image', 'document'],
output: ['text'],
endpoints: ['chat', 'batch', 'chat-completions', 'fine-tuning'],
features: [
Expand Down Expand Up @@ -925,7 +925,7 @@ const GPT4_1 = {
},
},
supports: {
input: ['text', 'image'],
input: ['text', 'image', 'document'],
output: ['text'],
endpoints: [
'chat',
Expand Down Expand Up @@ -978,7 +978,7 @@ const GPT4_1_MINI = {
},
},
supports: {
input: ['text', 'image'],
input: ['text', 'image', 'document'],
output: ['text'],
endpoints: [
'chat',
Expand Down Expand Up @@ -1029,7 +1029,7 @@ const GPT4_1_NANO = {
},
},
supports: {
input: ['text', 'image'],
input: ['text', 'image', 'document'],
output: ['text'],
endpoints: [
'chat',
Expand Down Expand Up @@ -1080,7 +1080,7 @@ const O1_PRO = {
},
},
supports: {
input: ['text', 'image'],
input: ['text', 'image', 'document'],
output: ['text'],
endpoints: ['chat', 'batch'],
features: ['function_calling', 'structured_outputs'],
Expand Down Expand Up @@ -1283,7 +1283,7 @@ const O1 = {
},
},
supports: {
input: ['text', 'image'],
input: ['text', 'image', 'document'],
output: ['text'],
endpoints: ['chat', 'batch', 'chat-completions', 'assistants'],
features: ['function_calling', 'structured_outputs', 'streaming'],
Expand Down Expand Up @@ -1333,7 +1333,7 @@ const GPT_4O = {
},
},
supports: {
input: ['text', 'image'],
input: ['text', 'image', 'document'],
output: ['text'],
endpoints: [
'chat',
Expand Down Expand Up @@ -1410,7 +1410,7 @@ const GPT_4O_MINI = {
},
},
supports: {
input: ['text', 'image'],
input: ['text', 'image', 'document'],
output: ['text'],
endpoints: [
'chat',
Expand Down Expand Up @@ -2048,7 +2048,7 @@ const GPT_5_5 = {
context_window: 1_050_000,
max_output_tokens: 128_000,
supports: {
input: ['image', 'text'],
input: ['text', 'image', 'document'],
output: ['text'],
endpoints: ['chat', 'chat-completions'],
features: [
Expand Down Expand Up @@ -2093,7 +2093,7 @@ const GPT_5_5_PRO = {
context_window: 1_050_000,
max_output_tokens: 128_000,
supports: {
input: ['image', 'text'],
input: ['text', 'image', 'document'],
output: ['text'],
endpoints: ['chat', 'chat-completions'],
features: [
Expand Down
Loading