Skip to content
Merged
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/tidy-pdfs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/ai-anthropic': patch
---

Serialize only Anthropic-supported fields from document and image metadata; use `filename` as a fallback document title.
16 changes: 14 additions & 2 deletions packages/ai-anthropic/src/adapters/text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -660,11 +660,14 @@ export class AnthropicTextAdapter<
return {
type: 'image',
source: imageSource,
...metadata,
...(metadata?.cache_control !== undefined && {
cache_control: metadata.cache_control,
}),
}
}
case 'document': {
const metadata = part.metadata as AnthropicDocumentMetadata | undefined
const title = metadata?.title ?? metadata?.filename
const docSource: Base64PDFSource | URLPDFSource =
part.source.type === 'data'
? {
Expand All @@ -679,7 +682,16 @@ export class AnthropicTextAdapter<
return {
type: 'document',
source: docSource,
...metadata,
...(metadata?.cache_control !== undefined && {
cache_control: metadata.cache_control,
}),
...(metadata?.citations !== undefined && {
citations: metadata.citations,
}),
...(metadata?.context !== undefined && {
context: metadata.context,
}),
...(title !== undefined && { title }),
}
}
case 'audio':
Expand Down
18 changes: 10 additions & 8 deletions packages/ai-anthropic/src/message-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import type {
CacheControlEphemeral,
CitationsConfigParam,
TextCitationParam,
} from '@anthropic-ai/sdk/resources'

Expand All @@ -25,10 +26,7 @@ export type AnthropicImageMediaType =
*/
export interface AnthropicImageMetadata {
/**
* The MIME type of the image.
* Required when using base64 source type.
*
* @see https://docs.anthropic.com/claude/docs/vision#supported-image-types
* @deprecated Ignored. The media type is taken from `source.mimeType`.
*/
mediaType?: AnthropicImageMediaType
/**
Expand Down Expand Up @@ -57,19 +55,23 @@ export type AnthropicDocumentMediaType = 'application/pdf'
*/
export interface AnthropicDocumentMetadata {
/**
* The MIME type of the document.
* Required for document content, typically 'application/pdf'.
*
* @see https://docs.anthropic.com/claude/docs/pdf-support
* @deprecated Ignored. The media type is taken from `source.mimeType`.
*/
mediaType?: AnthropicDocumentMediaType
/**
* Cache control settings for the document.
*/
cache_control?: CacheControlEphemeral

citations?: CitationsConfigParam

context?: string

/**
* The document filename. Used as the document title when `title` is absent.
*/
filename?: string

title?: string
}

Expand Down
98 changes: 98 additions & 0 deletions packages/ai-anthropic/tests/anthropic-adapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
} from '@tanstack/ai'
import { AnthropicTextAdapter } from '../src/adapters/text'
import type { AnthropicTextProviderOptions } from '../src/adapters/text'
import type { AnthropicDocumentMetadata } from '../src/message-types'
import { ANTHROPIC_MAX_NONSTREAMING_TOKENS } from '../src/model-meta'
import { z } from 'zod'

Expand Down Expand Up @@ -82,6 +83,103 @@ describe('Anthropic adapter option mapping', () => {
vi.clearAllMocks()
})

it('serializes only supported document metadata', async () => {
mocks.betaMessagesCreate.mockResolvedValueOnce(createTextStream('ok'))

const adapter = createAdapter('claude-opus-4-1')
const metadata = {
mediaType: 'application/pdf',
cache_control: { type: 'ephemeral' },
citations: { enabled: true },
context: 'Quarterly report',
filename: 'report.pdf',
placeholder: 'Attachment: report.pdf',
} as AnthropicDocumentMetadata & { placeholder: string }

for await (const _ of chat({
adapter,
messages: [
{
role: 'user',
content: [
{
type: 'document',
source: {
type: 'data',
value: 'JVBERi0xLjQ=',
mimeType: 'application/pdf',
},
metadata,
},
],
},
],
})) {
// consume stream
}

const [payload] = mocks.betaMessagesCreate.mock.calls[0]!

expect(payload.messages[0].content[0]).toEqual({
type: 'document',
source: {
type: 'base64',
data: 'JVBERi0xLjQ=',
media_type: 'application/pdf',
},
cache_control: { type: 'ephemeral' },
citations: { enabled: true },
context: 'Quarterly report',
title: 'report.pdf',
})
})
Comment thread
coderabbitai[bot] marked this conversation as resolved.

it('prefers document title over filename and serializes only cache_control for images', async () => {
mocks.betaMessagesCreate.mockResolvedValueOnce(createTextStream('ok'))

const adapter = createAdapter('claude-opus-4-1')

for await (const _ of chat({
adapter,
messages: [
{
role: 'user',
content: [
{
type: 'document',
source: {
type: 'data',
value: 'JVBERi0xLjQ=',
mimeType: 'application/pdf',
},
metadata: { title: 'Q3 Report', filename: 'report.pdf' },
},
{
type: 'image',
source: { type: 'url', value: 'https://example.com/a.png' },
metadata: {
mediaType: 'image/png',
cache_control: { type: 'ephemeral' },
filename: 'a.png',
},
},
],
},
],
})) {
// consume stream
}

const [payload] = mocks.betaMessagesCreate.mock.calls[0]!

expect(payload.messages[0].content[0].title).toBe('Q3 Report')
expect(payload.messages[0].content[1]).toEqual({
type: 'image',
source: { type: 'url', url: 'https://example.com/a.png' },
cache_control: { type: 'ephemeral' },
})
})

it('passes systemPrompts as TextBlockParam[] for prompt caching support', async () => {
const mockStream = (async function* () {
yield {
Expand Down
Loading