Skip to content

Commit fc50fc2

Browse files
committed
refactor(js/plugins/anthropic): factor out shared citation logic and add more live tests
1 parent 7e4c21a commit fc50fc2

File tree

4 files changed

+319
-145
lines changed

4 files changed

+319
-145
lines changed

js/plugins/anthropic/src/runner/converters/beta.ts

Lines changed: 12 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import type { BetaRequestDocumentBlock } from '@anthropic-ai/sdk/resources/beta/messages';
2222
import type { Part } from 'genkit';
2323
import type { AnthropicDocumentOptions } from '../../types.js';
24+
import { convertDocumentSource, createDocumentBlock } from './shared.js';
2425

2526
/**
2627
* Converts a server_tool_use block to a Genkit Part.
@@ -57,82 +58,19 @@ export function unsupportedServerToolError(blockType: string): string {
5758

5859
/**
5960
* Converts AnthropicDocumentOptions to Anthropic's beta API document block format.
61+
* The beta API supports file-based sources via the Files API.
6062
*/
6163
export function toBetaDocumentBlock(
6264
options: AnthropicDocumentOptions
6365
): BetaRequestDocumentBlock {
64-
const block: BetaRequestDocumentBlock = {
65-
type: 'document',
66-
source: toBetaDocumentSource(options.source),
67-
};
68-
69-
if (options.title) {
70-
block.title = options.title;
71-
}
72-
if (options.context) {
73-
block.context = options.context;
74-
}
75-
if (options.citations) {
76-
block.citations = options.citations;
77-
}
78-
79-
return block;
80-
}
81-
82-
/**
83-
* Converts document source options to Anthropic's beta API source format.
84-
* The beta API supports file-based sources via the Files API.
85-
*/
86-
function toBetaDocumentSource(
87-
source: AnthropicDocumentOptions['source']
88-
): BetaRequestDocumentBlock['source'] {
89-
switch (source.type) {
90-
case 'text':
91-
return {
92-
type: 'text',
93-
media_type: (source.mediaType ?? 'text/plain') as 'text/plain',
94-
data: source.data,
95-
};
96-
case 'base64':
97-
return {
98-
type: 'base64',
99-
media_type: source.mediaType as 'application/pdf',
100-
data: source.data,
101-
};
102-
case 'file':
103-
return {
104-
type: 'file',
105-
file_id: source.fileId,
106-
};
107-
case 'content':
108-
return {
109-
type: 'content',
110-
content: source.content.map((item) => {
111-
if (item.type === 'text') {
112-
return item;
113-
}
114-
return {
115-
type: 'image' as const,
116-
source: {
117-
type: 'base64' as const,
118-
media_type: item.source.mediaType as
119-
| 'image/jpeg'
120-
| 'image/png'
121-
| 'image/gif'
122-
| 'image/webp',
123-
data: item.source.data,
124-
},
125-
};
126-
}),
127-
};
128-
case 'url':
129-
return {
130-
type: 'url',
131-
url: source.url,
132-
};
133-
default:
134-
throw new Error(
135-
`Unsupported document source type: ${(source as { type: string }).type}`
136-
);
137-
}
66+
return createDocumentBlock<BetaRequestDocumentBlock>(options, (source) =>
67+
convertDocumentSource<BetaRequestDocumentBlock['source']>(
68+
source,
69+
(fileId) =>
70+
({
71+
type: 'file',
72+
file_id: fileId,
73+
}) as BetaRequestDocumentBlock['source']
74+
)
75+
);
13876
}

js/plugins/anthropic/src/runner/converters/shared.ts

Lines changed: 92 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,10 @@
2020
*/
2121

2222
import type { Part } from 'genkit';
23-
import type { AnthropicCitation } from '../../types.js';
23+
import type {
24+
AnthropicCitation,
25+
AnthropicDocumentOptions,
26+
} from '../../types.js';
2427

2528
/** Structural type for Anthropic citations (works with both stable and beta APIs). */
2629
interface AnthropicCitationInput {
@@ -214,3 +217,91 @@ export function inputJsonDeltaError(): Error {
214217
'Anthropic streaming tool input (input_json_delta) is not yet supported. Please disable streaming or upgrade this plugin.'
215218
);
216219
}
220+
221+
// --- Document block converters (shared between stable and beta APIs) ---
222+
223+
/**
224+
* Document block type constraint for generics.
225+
*/
226+
type DocumentBlockBase = {
227+
type: 'document';
228+
source: unknown;
229+
title?: string | null;
230+
context?: string | null;
231+
citations?: { enabled?: boolean } | null;
232+
};
233+
234+
/**
235+
* Converts AnthropicDocumentOptions to Anthropic's document block format.
236+
* Works for both stable and beta APIs via generics.
237+
*/
238+
export function createDocumentBlock<T extends DocumentBlockBase>(
239+
options: AnthropicDocumentOptions,
240+
sourceConverter: (source: AnthropicDocumentOptions['source']) => T['source']
241+
): T {
242+
return {
243+
type: 'document' as const,
244+
source: sourceConverter(options.source),
245+
...(options.title && { title: options.title }),
246+
...(options.context && { context: options.context }),
247+
...(options.citations && { citations: options.citations }),
248+
} as T;
249+
}
250+
251+
/**
252+
* Converts document source options to Anthropic's source format.
253+
* Works for both stable and beta APIs via a file handler callback.
254+
* The file handler is called for 'file' type sources, allowing different
255+
* behavior (error for stable, conversion for beta).
256+
*/
257+
export function convertDocumentSource<T>(
258+
source: AnthropicDocumentOptions['source'],
259+
fileHandler: (fileId: string) => T
260+
): T {
261+
switch (source.type) {
262+
case 'text':
263+
return {
264+
type: 'text',
265+
media_type: (source.mediaType ?? 'text/plain') as 'text/plain',
266+
data: source.data,
267+
} as T;
268+
case 'base64':
269+
return {
270+
type: 'base64',
271+
media_type: source.mediaType as 'application/pdf',
272+
data: source.data,
273+
} as T;
274+
case 'file':
275+
return fileHandler(source.fileId);
276+
case 'content':
277+
return {
278+
type: 'content',
279+
content: source.content.map((item) => {
280+
if (item.type === 'text') {
281+
return item;
282+
}
283+
return {
284+
type: 'image' as const,
285+
source: {
286+
type: 'base64' as const,
287+
media_type: item.source.mediaType as
288+
| 'image/jpeg'
289+
| 'image/png'
290+
| 'image/gif'
291+
| 'image/webp',
292+
data: item.source.data,
293+
},
294+
};
295+
}),
296+
} as T;
297+
case 'url':
298+
return {
299+
type: 'url',
300+
url: source.url,
301+
} as T;
302+
default:
303+
throw new Error(
304+
`Unsupported document source type: ${(source as { type: string }).type}`
305+
);
306+
}
307+
}

js/plugins/anthropic/src/runner/converters/stable.ts

Lines changed: 6 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import type { DocumentBlockParam } from '@anthropic-ai/sdk/resources/messages';
2222
import type { Part } from 'genkit';
2323
import type { AnthropicDocumentOptions } from '../../types.js';
24+
import { convertDocumentSource, createDocumentBlock } from './shared.js';
2425

2526
/**
2627
* Converts a server_tool_use block to a Genkit Part.
@@ -45,81 +46,16 @@ export function serverToolUseBlockToPart(block: {
4546

4647
/**
4748
* Converts AnthropicDocumentOptions to Anthropic's stable API document block format.
49+
* Note: The stable API does not support file-based sources (Files API).
4850
*/
4951
export function toDocumentBlock(
5052
options: AnthropicDocumentOptions
5153
): DocumentBlockParam {
52-
const block: DocumentBlockParam = {
53-
type: 'document',
54-
source: toDocumentSource(options.source),
55-
};
56-
57-
if (options.title) {
58-
block.title = options.title;
59-
}
60-
if (options.context) {
61-
block.context = options.context;
62-
}
63-
if (options.citations) {
64-
block.citations = options.citations;
65-
}
66-
67-
return block;
68-
}
69-
70-
/**
71-
* Converts document source options to Anthropic's stable API source format.
72-
* Note: The stable API does not support file-based sources (Files API).
73-
*/
74-
function toDocumentSource(
75-
source: AnthropicDocumentOptions['source']
76-
): DocumentBlockParam['source'] {
77-
switch (source.type) {
78-
case 'text':
79-
return {
80-
type: 'text',
81-
media_type: (source.mediaType ?? 'text/plain') as 'text/plain',
82-
data: source.data,
83-
};
84-
case 'base64':
85-
return {
86-
type: 'base64',
87-
media_type: source.mediaType as 'application/pdf',
88-
data: source.data,
89-
};
90-
case 'file':
54+
return createDocumentBlock<DocumentBlockParam>(options, (source) =>
55+
convertDocumentSource<DocumentBlockParam['source']>(source, () => {
9156
throw new Error(
9257
'File-based document sources require the beta API. Set apiVersion: "beta" in your plugin config or request config.'
9358
);
94-
case 'content':
95-
return {
96-
type: 'content',
97-
content: source.content.map((item) => {
98-
if (item.type === 'text') {
99-
return item;
100-
}
101-
return {
102-
type: 'image' as const,
103-
source: {
104-
type: 'base64' as const,
105-
media_type: item.source.mediaType as
106-
| 'image/jpeg'
107-
| 'image/png'
108-
| 'image/gif'
109-
| 'image/webp',
110-
data: item.source.data,
111-
},
112-
};
113-
}),
114-
};
115-
case 'url':
116-
return {
117-
type: 'url',
118-
url: source.url,
119-
};
120-
default:
121-
throw new Error(
122-
`Unsupported document source type: ${(source as { type: string }).type}`
123-
);
124-
}
59+
})
60+
);
12561
}

0 commit comments

Comments
 (0)