Skip to content

Commit b067e14

Browse files
authored
feat(js/plugins/anthropic): add support for citations (#4190)
1 parent 303cfb1 commit b067e14

File tree

15 files changed

+1851
-92
lines changed

15 files changed

+1851
-92
lines changed

js/plugins/anthropic/README.md

Lines changed: 111 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,16 @@
44

55
<h4 align="center">Anthropic AI plugin for Google Firebase Genkit</h4>
66

7-
`@genkit-ai/anthropic` is the official Anthropic plugin for [Firebase Genkit](https://github.com/firebase/genkit). It supersedes the earlier community package `genkitx-anthropic` and is now maintained by Google.
7+
`@genkit-ai/anthropic` is the official Anthropic plugin for
8+
[Firebase Genkit](https://github.com/firebase/genkit). It supersedes the earlier
9+
community package `genkitx-anthropic` and is now maintained by Google.
810

911
## Supported models
1012

11-
The plugin supports the most recent Anthropic models: **Claude Haiku 4.5**, **Claude Sonnet 4.5**, and **Claude Opus 4.5**. Additionally, the plugin supports all of the [non-retired older models](https://platform.claude.com/docs/en/about-claude/model-deprecations#model-status).
13+
The plugin supports the most recent Anthropic models: **Claude Haiku 4.5**,
14+
**Claude Sonnet 4.5**, and **Claude Opus 4.5**. Additionally, the plugin
15+
supports all of the
16+
[non-retired older models](https://platform.claude.com/docs/en/about-claude/model-deprecations#model-status).
1217

1318
## Installation
1419

@@ -23,13 +28,13 @@ Install the plugin in your project with your favorite package manager:
2328
### Initialize
2429

2530
```typescript
26-
import { genkit } from 'genkit';
27-
import { anthropic } from '@genkit-ai/anthropic';
31+
import { genkit } from "genkit";
32+
import { anthropic } from "@genkit-ai/anthropic";
2833

2934
const ai = genkit({
3035
plugins: [anthropic({ apiKey: process.env.ANTHROPIC_API_KEY })],
3136
// specify a default model for generate here if you wish:
32-
model: anthropic.model('claude-sonnet-4-5'),
37+
model: anthropic.model("claude-sonnet-4-5"),
3338
});
3439
```
3540

@@ -39,8 +44,8 @@ The simplest way to generate text is by using the `generate` method:
3944

4045
```typescript
4146
const response = await ai.generate({
42-
model: anthropic.model('claude-haiku-4-5'),
43-
prompt: 'Tell me a joke.',
47+
model: anthropic.model("claude-haiku-4-5"),
48+
prompt: "Tell me a joke.",
4449
});
4550

4651
console.log(response.text);
@@ -53,7 +58,7 @@ console.log(response.text);
5358

5459
const response = await ai.generate({
5560
prompt: [
56-
{ text: 'What animal is in the photo?' },
61+
{ text: "What animal is in the photo?" },
5762
{ media: { url: imageUrl } },
5863
],
5964
});
@@ -62,11 +67,12 @@ console.log(response.text);
6267

6368
### Extended thinking
6469

65-
Claude 4.5 models can expose their internal reasoning. Enable it per-request with the Anthropic thinking config and read the reasoning from the response:
70+
Claude 4.5 models can expose their internal reasoning. Enable it per-request
71+
with the Anthropic thinking config and read the reasoning from the response:
6672

6773
```typescript
6874
const response = await ai.generate({
69-
prompt: 'Walk me through your reasoning for Fermat’s little theorem.',
75+
prompt: "Walk me through your reasoning for Fermat’s little theorem.",
7076
config: {
7177
thinking: {
7278
enabled: true,
@@ -75,15 +81,76 @@ const response = await ai.generate({
7581
},
7682
});
7783

78-
console.log(response.text); // Final assistant answer
79-
console.log(response.reasoning); // Summarized thinking steps
84+
console.log(response.text); // Final assistant answer
85+
console.log(response.reasoning); // Summarized thinking steps
8086
```
8187

82-
When thinking is enabled, request bodies sent through the plugin include the `thinking` payload (`{ type: 'enabled', budget_tokens: … }`) that Anthropic's API expects, and streamed responses deliver `reasoning` parts as they arrive so you can render the chain-of-thought incrementally.
88+
When thinking is enabled, request bodies sent through the plugin include the
89+
`thinking` payload (`{ type: 'enabled', budget_tokens: … }`) that Anthropic's
90+
API expects, and streamed responses deliver `reasoning` parts as they arrive so
91+
you can render the chain-of-thought incrementally.
92+
93+
### Document Citations
94+
95+
Claude can cite specific parts of documents you provide, making it easy to trace
96+
where information in the response came from. Use the `anthropicDocument()`
97+
helper to create citable documents. For more details, see the
98+
[Anthropic Citations documentation](https://platform.claude.com/docs/en/build-with-claude/citations).
99+
100+
```typescript
101+
import { anthropic, anthropicDocument } from "@genkit-ai/anthropic";
102+
103+
const response = await ai.generate({
104+
model: anthropic.model("claude-sonnet-4-5"),
105+
messages: [
106+
{
107+
role: "user",
108+
content: [
109+
anthropicDocument({
110+
source: {
111+
type: "text",
112+
data: "The grass is green. The sky is blue. Water is wet.",
113+
},
114+
title: "Basic Facts",
115+
citations: { enabled: true },
116+
}),
117+
{ text: "What color is the grass? Cite your source." },
118+
],
119+
},
120+
],
121+
});
122+
123+
// Access citations from response parts
124+
const citations = response.message?.content?.flatMap(
125+
(part) => part.metadata?.citations || [],
126+
) ?? [];
127+
128+
console.log("Citations:", citations);
129+
```
130+
131+
**Important:** Citations must be enabled on all documents in a request, or on
132+
none of them. You cannot mix documents with citations enabled and disabled in
133+
the same request.
134+
135+
Supported document source types:
136+
137+
- `text` - Plain text documents (returns `char_location` citations)
138+
- `base64` - Base64-encoded PDFs (returns `page_location` citations)
139+
- `url` - PDFs accessible via URL (returns `page_location` citations)
140+
- `content` - Custom content blocks with text/images (returns
141+
`content_block_location` citations)
142+
- `file` - File references from Anthropic's Files API, beta API only (returns
143+
`page_location` citations)
144+
145+
Citations are returned in the response parts' metadata and include information
146+
about the document index, cited text, and location (character indices, page
147+
numbers, or block indices depending on the source type).
83148

84149
### Beta API Limitations
85150

86-
The beta API surface provides access to experimental features, but some server-managed tool blocks are not yet supported by this plugin. The following beta API features will cause an error if encountered:
151+
The beta API surface provides access to experimental features, but some
152+
server-managed tool blocks are not yet supported by this plugin. The following
153+
beta API features will cause an error if encountered:
87154

88155
- `web_fetch_tool_result`
89156
- `code_execution_tool_result`
@@ -93,18 +160,19 @@ The beta API surface provides access to experimental features, but some server-m
93160
- `mcp_tool_use`
94161
- `container_upload`
95162

96-
Note that `server_tool_use` and `web_search_tool_result` ARE supported and work with both stable and beta APIs.
163+
Note that `server_tool_use` and `web_search_tool_result` ARE supported and work
164+
with both stable and beta APIs.
97165

98166
### Within a flow
99167

100168
```typescript
101-
import { z } from 'genkit';
169+
import { z } from "genkit";
102170

103171
// ...initialize Genkit instance (as shown above)...
104172

105173
export const jokeFlow = ai.defineFlow(
106174
{
107-
name: 'jokeFlow',
175+
name: "jokeFlow",
108176
inputSchema: z.string(),
109177
outputSchema: z.string(),
110178
},
@@ -113,26 +181,27 @@ export const jokeFlow = ai.defineFlow(
113181
prompt: `tell me a joke about ${subject}`,
114182
});
115183
return llmResponse.text;
116-
}
184+
},
117185
);
118186
```
119187

120188
### Direct model usage (without Genkit instance)
121189

122-
The plugin supports Genkit Plugin API v2, which allows you to use models directly without initializing the full Genkit framework:
190+
The plugin supports Genkit Plugin API v2, which allows you to use models
191+
directly without initializing the full Genkit framework:
123192

124193
```typescript
125-
import { anthropic } from '@genkit-ai/anthropic';
194+
import { anthropic } from "@genkit-ai/anthropic";
126195

127196
// Create a model reference directly
128-
const claude = anthropic.model('claude-sonnet-4-5');
197+
const claude = anthropic.model("claude-sonnet-4-5");
129198

130199
// Use the model directly
131200
const response = await claude({
132201
messages: [
133202
{
134-
role: 'user',
135-
content: [{ text: 'Tell me a joke.' }],
203+
role: "user",
204+
content: [{ text: "Tell me a joke." }],
136205
},
137206
],
138207
});
@@ -143,19 +212,19 @@ console.log(response);
143212
You can also create model references using the plugin's `model()` method:
144213

145214
```typescript
146-
import { anthropic } from '@genkit-ai/anthropic';
215+
import { anthropic } from "@genkit-ai/anthropic";
147216

148217
// Create model references
149-
const claudeHaiku45 = anthropic.model('claude-haiku-4-5');
150-
const claudeSonnet45 = anthropic.model('claude-sonnet-4-5');
151-
const claudeOpus45 = anthropic.model('claude-opus-4-5');
218+
const claudeHaiku45 = anthropic.model("claude-haiku-4-5");
219+
const claudeSonnet45 = anthropic.model("claude-sonnet-4-5");
220+
const claudeOpus45 = anthropic.model("claude-opus-4-5");
152221

153222
// Use the model reference directly
154223
const response = await claudeSonnet45({
155224
messages: [
156225
{
157-
role: 'user',
158-
content: [{ text: 'Hello!' }],
226+
role: "user",
227+
content: [{ text: "Hello!" }],
159228
},
160229
],
161230
});
@@ -169,22 +238,29 @@ This approach is useful for:
169238

170239
## Acknowledgements
171240

172-
This plugin builds on the community work published as [`genkitx-anthropic`](https://github.com/BloomLabsInc/genkit-plugins/blob/main/plugins/anthropic/README.md) by Bloom Labs Inc. Their Apache 2.0–licensed implementation provided the foundation for this maintained package.
241+
This plugin builds on the community work published as
242+
[`genkitx-anthropic`](https://github.com/BloomLabsInc/genkit-plugins/blob/main/plugins/anthropic/README.md)
243+
by Bloom Labs Inc. Their Apache 2.0–licensed implementation provided the
244+
foundation for this maintained package.
173245

174246
## Contributing
175247

176-
Want to contribute to the project? That's awesome! Head over to our [Contribution Guidelines](CONTRIBUTING.md).
248+
Want to contribute to the project? That's awesome! Head over to our
249+
[Contribution Guidelines](CONTRIBUTING.md).
177250

178251
## Need support?
179252

180253
> [!NOTE]
181-
> This repository depends on Google's Firebase Genkit. For issues and questions related to Genkit, please refer to instructions available in [Genkit's repository](https://github.com/firebase/genkit).
182-
254+
> This repository depends on Google's Firebase Genkit. For issues and questions
255+
> related to Genkit, please refer to instructions available in
256+
> [Genkit's repository](https://github.com/firebase/genkit).
183257
184258
## Credits
185259

186-
This plugin is maintained by Google with acknowledgement to the community contributions from [Bloom Labs Inc](https://github.com/BloomLabsInc).
260+
This plugin is maintained by Google with acknowledgement to the community
261+
contributions from [Bloom Labs Inc](https://github.com/BloomLabsInc).
187262

188263
## License
189264

190-
This project is licensed under the [Apache 2.0 License](https://github.com/BloomLabsInc/genkit-plugins/blob/main/LICENSE).
265+
This project is licensed under the
266+
[Apache 2.0 License](https://github.com/BloomLabsInc/genkit-plugins/blob/main/LICENSE).

js/plugins/anthropic/src/index.ts

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import Anthropic from '@anthropic-ai/sdk';
1919
import { genkitPluginV2, type GenkitPluginV2 } from 'genkit/plugin';
2020

21+
import type { Part } from 'genkit';
2122
import { ActionMetadata, ModelReference, z } from 'genkit';
2223
import { ModelAction } from 'genkit/model';
2324
import { ActionType } from 'genkit/registry';
@@ -31,7 +32,15 @@ import {
3132
claudeModel,
3233
claudeModelReference,
3334
} from './models.js';
34-
import { InternalPluginOptions, PluginOptions, __testClient } from './types.js';
35+
import {
36+
InternalPluginOptions,
37+
PluginOptions,
38+
__testClient,
39+
type AnthropicDocumentOptions,
40+
} from './types.js';
41+
42+
// Re-export citation type for consumers (AnthropicDocumentOptions is inferred via anthropicDocument())
43+
export type { AnthropicCitation } from './types.js';
3544

3645
const PROMPT_CACHING_BETA_HEADER_VALUE = 'prompt-caching-2024-07-31';
3746

@@ -152,4 +161,38 @@ export const anthropic = anthropicPlugin as AnthropicPlugin;
152161
return claudeModelReference(name, config);
153162
};
154163

164+
/**
165+
* Creates a custom part representing an Anthropic document with optional citations support.
166+
*
167+
* Use this to provide documents to Claude that can be cited in responses.
168+
* Citations must be enabled on all or none of the documents in a request.
169+
*
170+
* @example
171+
* ```ts
172+
* import { anthropic, anthropicDocument } from '@genkit-ai/anthropic';
173+
*
174+
* const { text } = await ai.generate({
175+
* model: anthropic.model('claude-sonnet-4-5'),
176+
* messages: [{
177+
* role: 'user',
178+
* content: [
179+
* anthropicDocument({
180+
* source: { type: 'text', data: 'The grass is green. The sky is blue.' },
181+
* title: 'Nature Facts',
182+
* citations: { enabled: true }
183+
* }),
184+
* { text: 'What color is the grass?' }
185+
* ]
186+
* }]
187+
* });
188+
* ```
189+
*/
190+
export function anthropicDocument(options: AnthropicDocumentOptions): Part {
191+
return {
192+
custom: {
193+
anthropicDocument: options,
194+
},
195+
};
196+
}
197+
155198
export default anthropic;

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

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,14 +43,20 @@ import type {
4343
import { logger } from 'genkit/logging';
4444

4545
import { KNOWN_CLAUDE_MODELS, extractVersion } from '../models.js';
46-
import { AnthropicConfigSchema, type ClaudeRunnerParams } from '../types.js';
46+
import {
47+
AnthropicConfigSchema,
48+
type AnthropicDocumentOptions,
49+
type ClaudeRunnerParams,
50+
} from '../types.js';
4751
import { removeUndefinedProperties } from '../utils.js';
4852
import { BaseRunner } from './base.js';
4953
import {
5054
betaServerToolUseBlockToPart,
55+
toBetaDocumentBlock,
5156
unsupportedServerToolError,
5257
} from './converters/beta.js';
5358
import {
59+
citationsDeltaToPart,
5460
inputJsonDeltaError,
5561
redactedThinkingBlockToPart,
5662
textBlockToPart,
@@ -182,6 +188,13 @@ export class BetaRunner extends BaseRunner<BetaRunnerTypes> {
182188
return { type: 'text', text: part.text };
183189
}
184190

191+
// Custom document (for citations support)
192+
if (part.custom?.anthropicDocument) {
193+
return toBetaDocumentBlock(
194+
part.custom.anthropicDocument as AnthropicDocumentOptions
195+
);
196+
}
197+
185198
// Media
186199
if (part.media) {
187200
if (part.media.contentType === 'anthropic/file') {
@@ -463,6 +476,9 @@ export class BetaRunner extends BaseRunner<BetaRunnerTypes> {
463476
if (event.delta.type === 'thinking_delta') {
464477
return thinkingDeltaToPart(event.delta);
465478
}
479+
if (event.delta.type === 'citations_delta') {
480+
return citationsDeltaToPart(event.delta);
481+
}
466482
if (event.delta.type === 'input_json_delta') {
467483
throw inputJsonDeltaError();
468484
}

0 commit comments

Comments
 (0)