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
5 changes: 5 additions & 0 deletions .changeset/mistral-document-input.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/ai-mistral': minor
---

Support document (PDF) input for Mistral vision models via `document_url`. The text adapter now maps `document` content parts to Mistral's `document_url` format (hosted URLs pass through; inline bytes are wrapped in a `data:` URL, mirroring image handling), and `document` is declared as an input modality for the vision-capable models. Previously any `document` content part threw "Supported types: text, image".
11 changes: 10 additions & 1 deletion packages/ai-mistral/src/adapters/text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1019,8 +1019,17 @@ export class MistralTextAdapter<
}
}

if (part.type === 'document') {
const documentValue = part.source.value
const documentUrl =
part.source.type === 'data' && !documentValue.startsWith('data:')
? `data:${part.source.mimeType};base64,${documentValue}`
: documentValue
return { type: 'document_url', documentUrl }
}

throw new Error(
`Mistral text adapter does not support content part of type '${(part as ContentPart).type}'. Supported types: text, image. Use a vision-capable model (pixtral-large-latest, pixtral-12b-2409, mistral-medium-latest, or mistral-small-latest) for images.`,
`Mistral text adapter does not support content part of type '${(part as ContentPart).type}'. Supported types: text, image, document. Use a vision-capable model (mistral-medium-latest or mistral-small-latest) for images and documents.`,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Error message understates which models actually support images/documents.

The message only names mistral-medium-latest and mistral-small-latest, but model-meta.ts shows pixtral-large-latest and pixtral-12b-2409 also declare vision in their features and now include document in supports.input. Users on Pixtral models hitting this error would be pointed to the wrong models.

📝 Proposed fix
-      `Mistral text adapter does not support content part of type '${(part as ContentPart).type}'. Supported types: text, image, document. Use a vision-capable model (mistral-medium-latest or mistral-small-latest) for images and documents.`,
+      `Mistral text adapter does not support content part of type '${part.type}'. Supported types: text, image, document. Use a vision-capable model (mistral-medium-latest, mistral-small-latest, pixtral-large-latest, or pixtral-12b-2409) for images and documents.`,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
`Mistral text adapter does not support content part of type '${(part as ContentPart).type}'. Supported types: text, image, document. Use a vision-capable model (mistral-medium-latest or mistral-small-latest) for images and documents.`,
`Mistral text adapter does not support content part of type '${part.type}'. Supported types: text, image, document. Use a vision-capable model (mistral-medium-latest, mistral-small-latest, pixtral-large-latest, or pixtral-12b-2409) for images and documents.`,
🧰 Tools
🪛 ESLint

[error] 1032-1032: This assertion is unnecessary since it does not change the type of the expression.

(@typescript-eslint/no-unnecessary-type-assertion)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ai-mistral/src/adapters/text.ts` at line 1032, The unsupported
content error in the Mistral text adapter is too narrow and points users only to
mistral-medium-latest and mistral-small-latest. Update the message in the text
adapter logic to reflect all vision-capable models that support
images/documents, including pixtral-large-latest and pixtral-12b-2409 as defined
in model-meta.ts. Keep the wording aligned with the actual model capability list
so the error guidance matches the supported models.

)
}

Expand Down
10 changes: 5 additions & 5 deletions packages/ai-mistral/src/model-meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ interface ModelMeta<TProviderOptions = unknown> {
output?: { normal: number }
}
supports: {
input: Array<'text' | 'image' | 'audio'>
input: Array<'text' | 'image' | 'audio' | 'document'>
output: Array<'text'>
endpoints: Array<'chat' | 'embeddings'>

Expand Down Expand Up @@ -61,7 +61,7 @@ const MISTRAL_MEDIUM_LATEST = {
output: { normal: 2 },
},
supports: {
input: ['text', 'image'],
input: ['text', 'image', 'document'],
output: ['text'],
endpoints: ['chat'],
features: ['streaming', 'tools', 'json_object', 'json_schema', 'vision'],
Expand All @@ -77,7 +77,7 @@ const MISTRAL_SMALL_LATEST = {
output: { normal: 0.3 },
},
supports: {
input: ['text', 'image'],
input: ['text', 'image', 'document'],
output: ['text'],
endpoints: ['chat'],
features: ['streaming', 'tools', 'json_object', 'json_schema', 'vision'],
Expand Down Expand Up @@ -141,7 +141,7 @@ const PIXTRAL_LARGE_LATEST = {
output: { normal: 6 },
},
supports: {
input: ['text', 'image'],
input: ['text', 'image', 'document'],
output: ['text'],
endpoints: ['chat'],
features: ['streaming', 'tools', 'json_object', 'json_schema', 'vision'],
Expand All @@ -157,7 +157,7 @@ const PIXTRAL_12B_2409 = {
output: { normal: 0.15 },
},
supports: {
input: ['text', 'image'],
input: ['text', 'image', 'document'],
output: ['text'],
endpoints: ['chat'],
features: ['streaming', 'tools', 'json_object', 'vision'],
Expand Down
64 changes: 64 additions & 0 deletions packages/ai-mistral/tests/document-input.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { describe, expect, it } from 'vitest'
import { createMistralText } from '../src/adapters/text'
import type { DocumentPart } from '@tanstack/ai'

// `convertContentPartToMistral` is private; reach it directly so the test
// exercises the document mapping without standing up an HTTP mock. The
// `messageToWire` step then snake-cases `documentUrl` -> `document_url`.
const convertContentPart = (
adapter: ReturnType<typeof createMistralText>,
part: DocumentPart,
): unknown =>
(
adapter as unknown as {
convertContentPartToMistral: (p: DocumentPart) => unknown
}
).convertContentPartToMistral(part)

describe('mistral document content parts', () => {
const adapter = createMistralText('mistral-small-latest', 'test-key')

it('maps a hosted-URL document to a document_url part', () => {
const part: DocumentPart = {
type: 'document',
source: { type: 'url', value: 'https://example.com/contract.pdf' },
}

expect(convertContentPart(adapter, part)).toEqual({
type: 'document_url',
documentUrl: 'https://example.com/contract.pdf',
})
})

it('wraps inline base64 document bytes in a data: URL', () => {
const part: DocumentPart = {
type: 'document',
source: {
type: 'data',
value: 'JVBERi0xLjQ=',
mimeType: 'application/pdf',
},
}

expect(convertContentPart(adapter, part)).toEqual({
type: 'document_url',
documentUrl: 'data:application/pdf;base64,JVBERi0xLjQ=',
})
})

it('passes through a data: URL that is already formatted', () => {
const part: DocumentPart = {
type: 'document',
source: {
type: 'data',
value: 'data:application/pdf;base64,JVBERi0xLjQ=',
mimeType: 'application/pdf',
},
}

expect(convertContentPart(adapter, part)).toEqual({
type: 'document_url',
documentUrl: 'data:application/pdf;base64,JVBERi0xLjQ=',
})
})
})
Loading