Anthropic (Claude) Integration
Initializing the Client
src/lib/anthropic.js
import Anthropic from '@anthropic-ai/sdk';
import { ANTHROPIC_API_KEY } from '../config/env.js';
const anthropic = new Anthropic({
apiKey: ANTHROPIC_API_KEY,
timeout: 60_000,
maxRetries: 2,
});
export default anthropic;
Messages API
Anthropic's API has two notable differences from OpenAI:
systemis a top-level field, not a message role inside the arraymax_tokensis required — there is no default
src/services/anthropic.service.js
import anthropic from '../lib/anthropic.js';
export async function chat({
messages,
model = 'claude-sonnet-4-6',
systemPrompt = '',
maxTokens = 1024,
temperature = 1, // Anthropic default is 1; range is 0–1
}) {
const response = await anthropic.messages.create({
model,
max_tokens: maxTokens,
temperature,
system: systemPrompt,
messages,
});
return {
content: response.content[0].text,
usage: response.usage,
stopReason: response.stop_reason,
};
}
stop_reason values:
| Value | Meaning |
|---|---|
end_turn | Normal completion |
max_tokens | Hit the max_tokens limit — response cut off |
stop_sequence | A configured stop sequence was encountered |
tool_use | Model wants to call a tool |
Streaming with SSE
src/routes/claude.route.js — streaming
import anthropic from '../lib/anthropic.js';
router.post('/stream', async (req, res, next) => {
try {
const { messages, systemPrompt } = req.body;
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
res.setHeader('X-Accel-Buffering', 'no');
const stream = anthropic.messages.stream({
model: 'claude-sonnet-4-6',
max_tokens: 1024,
system: systemPrompt || '',
messages,
});
for await (const event of stream) {
if (event.type === 'content_block_delta' && event.delta.type === 'text_delta') {
res.write(`data: ${JSON.stringify({ token: event.delta.text })}\n\n`);
}
if (event.type === 'message_stop') {
res.write(`data: ${JSON.stringify({ done: true })}\n\n`);
}
}
res.end();
} catch (err) {
if (res.headersSent) {
res.write(`data: ${JSON.stringify({ error: err.message })}\n\n`);
res.end();
} else {
next(err);
}
}
});
Anthropic SSE event types:
| Event type | When it fires |
|---|---|
message_start | Start of the response with initial metadata |
content_block_start | A new content block begins (text or tool) |
content_block_delta | A token delta — the actual streamed text |
content_block_stop | A content block ends |
message_delta | Final usage stats |
message_stop | Stream fully complete |
Vision with Claude
Sending an image URL to Claude
const response = await anthropic.messages.create({
model: 'claude-opus-4-7',
max_tokens: 1024,
messages: [
{
role: 'user',
content: [
{
type: 'image',
source: { type: 'url', url: 'https://example.com/diagram.png' },
},
{ type: 'text', text: 'Explain this architecture diagram.' },
],
},
],
});
Sending a base64 image to Claude
const response = await anthropic.messages.create({
model: 'claude-opus-4-7',
max_tokens: 1024,
messages: [
{
role: 'user',
content: [
{
type: 'image',
source: {
type: 'base64',
media_type: 'image/jpeg', // 'image/jpeg' | 'image/png' | 'image/gif' | 'image/webp'
data: base64String,
},
},
{ type: 'text', text: 'What does this show?' },
],
},
],
});
Structured Output
Claude does not have a native JSON schema enforcement mode, but it follows JSON instructions reliably when prompted correctly:
JSON extraction with Claude
const response = await anthropic.messages.create({
model: 'claude-sonnet-4-6',
max_tokens: 512,
system: 'You are a data extraction assistant. Always respond with valid JSON only. No explanation, no markdown, just JSON.',
messages: [
{
role: 'user',
content: 'Extract name and email from: "Contact Jane at jane@example.com"',
},
],
});
const data = JSON.parse(response.content[0].text);