@langchain/anthropic
Version:
Anthropic integrations for LangChain.js
328 lines (326 loc) • 13.4 kB
JavaScript
import { _isAnthropicCompactionBlock, _isAnthropicImageBlockParam, _isAnthropicRedactedThinkingBlock, _isAnthropicSearchResultBlock, _isAnthropicThinkingBlock, standardContentBlockConverter } from "./content.js";
import { _formatStandardContent } from "./standard.js";
import { AIMessage, HumanMessage, convertToProviderContentBlock, isDataContentBlock, parseBase64DataUrl } from "@langchain/core/messages";
//#region src/utils/message_inputs.ts
function _formatImage(imageUrl) {
const parsed = parseBase64DataUrl({ dataUrl: imageUrl });
if (parsed) return {
type: "base64",
media_type: parsed.mime_type,
data: parsed.data
};
let parsedUrl;
try {
parsedUrl = new URL(imageUrl);
} catch {
throw new Error([
`Malformed image URL: ${JSON.stringify(imageUrl)}. Content blocks of type 'image_url' must be a valid http, https, or base64-encoded data URL.`,
"Example: data:image/png;base64,/9j/4AAQSk...",
"Example: https://example.com/image.jpg"
].join("\n\n"));
}
if (parsedUrl.protocol === "http:" || parsedUrl.protocol === "https:") return {
type: "url",
url: imageUrl
};
throw new Error([
`Invalid image URL protocol: ${JSON.stringify(parsedUrl.protocol)}. Anthropic only supports images as http, https, or base64-encoded data URLs on 'image_url' content blocks.`,
"Example: data:image/png;base64,/9j/4AAQSk...",
"Example: https://example.com/image.jpg"
].join("\n\n"));
}
function _ensureMessageContents(messages) {
const updatedMsgs = [];
for (const message of messages) if (message._getType() === "tool") if (typeof message.content === "string") {
const previousMessage = updatedMsgs[updatedMsgs.length - 1];
if (previousMessage?._getType() === "human" && Array.isArray(previousMessage.content) && "type" in previousMessage.content[0] && previousMessage.content[0].type === "tool_result") previousMessage.content.push({
type: "tool_result",
content: message.content,
tool_use_id: message.tool_call_id
});
else updatedMsgs.push(new HumanMessage({ content: [{
type: "tool_result",
content: message.content,
tool_use_id: message.tool_call_id
}] }));
} else updatedMsgs.push(new HumanMessage({ content: [{
type: "tool_result",
...message.content != null ? { content: _formatContent(message) } : {},
tool_use_id: message.tool_call_id
}] }));
else updatedMsgs.push(message);
return updatedMsgs;
}
function _convertLangChainToolCallToAnthropic(toolCall) {
if (toolCall.id === void 0) throw new Error(`Anthropic requires all tool calls to have an "id".`);
return {
type: "tool_use",
id: toolCall.id,
name: toolCall.name,
input: toolCall.args
};
}
function* _formatContentBlocks(content, toolCalls) {
const toolTypes = [
"bash_code_execution_tool_result",
"input_json_delta",
"server_tool_use",
"text_editor_code_execution_tool_result",
"tool_result",
"tool_use",
"web_search_result",
"web_search_tool_result"
];
const textTypes = ["text", "text_delta"];
for (const contentPart of content) {
if (isDataContentBlock(contentPart)) yield convertToProviderContentBlock(contentPart, standardContentBlockConverter);
const cacheControl = "cache_control" in contentPart ? contentPart.cache_control : void 0;
if (contentPart.type === "image_url") {
let source;
if (typeof contentPart.image_url === "string") source = _formatImage(contentPart.image_url);
else if (typeof contentPart.image_url === "object" && contentPart.image_url !== null && "url" in contentPart.image_url && typeof contentPart.image_url.url === "string") source = _formatImage(contentPart.image_url.url);
if (source) yield {
type: "image",
source,
...cacheControl ? { cache_control: cacheControl } : {}
};
} else if (_isAnthropicImageBlockParam(contentPart)) yield contentPart;
else if (contentPart.type === "image") {
let source;
if ("url" in contentPart && typeof contentPart.url === "string") source = _formatImage(contentPart.url);
else if ("data" in contentPart && (typeof contentPart.data === "string" || contentPart.data instanceof Uint8Array)) source = {
type: "base64",
media_type: "mimeType" in contentPart && typeof contentPart.mimeType === "string" ? contentPart.mimeType : "image/jpeg",
data: typeof contentPart.data === "string" ? contentPart.data : Buffer.from(contentPart.data).toString("base64")
};
else if ("fileId" in contentPart && typeof contentPart.fileId === "string") source = {
type: "file",
file_id: contentPart.fileId
};
if (source) yield {
type: "image",
source,
...cacheControl ? { cache_control: cacheControl } : {}
};
} else if (contentPart.type === "file") {
let source;
if ("url" in contentPart && typeof contentPart.url === "string") source = {
type: "url",
url: contentPart.url
};
else if ("data" in contentPart && (typeof contentPart.data === "string" || contentPart.data instanceof Uint8Array)) source = {
type: "base64",
media_type: "mimeType" in contentPart && typeof contentPart.mimeType === "string" ? contentPart.mimeType : "application/pdf",
data: typeof contentPart.data === "string" ? contentPart.data : Buffer.from(contentPart.data).toString("base64")
};
else if ("fileId" in contentPart && typeof contentPart.fileId === "string") source = {
type: "file",
file_id: contentPart.fileId
};
if (source) yield {
type: "document",
source,
...cacheControl ? { cache_control: cacheControl } : {}
};
} else if (contentPart.type === "document") yield {
...contentPart,
...cacheControl ? { cache_control: cacheControl } : {}
};
else if (_isAnthropicThinkingBlock(contentPart)) yield {
type: "thinking",
thinking: contentPart.thinking,
signature: contentPart.signature,
...cacheControl ? { cache_control: cacheControl } : {}
};
else if (_isAnthropicRedactedThinkingBlock(contentPart)) yield {
type: "redacted_thinking",
data: contentPart.data,
...cacheControl ? { cache_control: cacheControl } : {}
};
else if (_isAnthropicCompactionBlock(contentPart)) yield {
type: "compaction",
content: contentPart.content,
...cacheControl ? { cache_control: cacheControl } : {}
};
else if (_isAnthropicSearchResultBlock(contentPart)) yield {
type: "search_result",
title: contentPart.title,
source: contentPart.source,
..."cache_control" in contentPart && contentPart.cache_control ? { cache_control: contentPart.cache_control } : {},
..."citations" in contentPart && contentPart.citations ? { citations: contentPart.citations } : {},
content: contentPart.content
};
else if (textTypes.find((t) => t === contentPart.type) && "text" in contentPart) yield {
type: "text",
text: contentPart.text,
...cacheControl ? { cache_control: cacheControl } : {},
..."citations" in contentPart && contentPart.citations ? { citations: contentPart.citations } : {}
};
else if (toolTypes.find((t) => t === contentPart.type)) {
const contentPartCopy = { ...contentPart };
if (contentPartCopy.type === "input_json_delta") continue;
if (contentPartCopy.type === "tool_use" && typeof contentPartCopy.input === "string") {
const matchingToolCall = toolCalls?.find((tc) => tc.id === contentPartCopy.id);
if (matchingToolCall) contentPartCopy.input = matchingToolCall.args;
else contentPartCopy.input = content.filter((nestedContentPart) => nestedContentPart.index === contentPartCopy.index && nestedContentPart.type === "input_json_delta" && typeof nestedContentPart.input === "string").reduce((accumulator, nestedContentPart) => accumulator + nestedContentPart.input, contentPartCopy.input);
}
if ("index" in contentPartCopy) delete contentPartCopy.index;
if ("input" in contentPartCopy) {
if (typeof contentPartCopy.input === "string") try {
contentPartCopy.input = JSON.parse(contentPartCopy.input);
} catch {
contentPartCopy.input = {};
}
}
yield {
...contentPartCopy,
...cacheControl ? { cache_control: cacheControl } : {}
};
} else if (contentPart.type === "container_upload") yield {
...contentPart,
...cacheControl ? { cache_control: cacheControl } : {}
};
}
}
function _formatContent(message, toolCalls) {
const { content } = message;
if (typeof content === "string") return content;
else return Array.from(_formatContentBlocks(content, toolCalls));
}
/**
* Formats messages as a prompt for the model.
* Used in LangSmith, export is important here.
* @param messages The base messages to format as a prompt.
* @returns The formatted prompt.
*/
function _convertMessagesToAnthropicPayload(messages) {
const mergedMessages = _ensureMessageContents(messages);
let system;
if (mergedMessages.length > 0 && mergedMessages[0]._getType() === "system") system = messages[0].content;
return {
messages: mergeMessages((system !== void 0 ? mergedMessages.slice(1) : mergedMessages).map((message) => {
let role;
if (message._getType() === "human") role = "user";
else if (message._getType() === "ai") role = "assistant";
else if (message._getType() === "tool") role = "user";
else if (message._getType() === "system") throw new Error("System messages are only permitted as the first passed message.");
else throw new Error(`Message type "${message.type}" is not supported.`);
if (AIMessage.isInstance(message) && message.response_metadata?.output_version === "v1") return {
role,
content: _formatStandardContent(message)
};
if (AIMessage.isInstance(message) && !!message.tool_calls?.length) if (typeof message.content === "string") if (message.content === "") return {
role,
content: message.tool_calls.map(_convertLangChainToolCallToAnthropic)
};
else return {
role,
content: [{
type: "text",
text: message.content
}, ...message.tool_calls.map(_convertLangChainToolCallToAnthropic)]
};
else {
const { content } = message;
const formattedContent = _formatContent(message, message.tool_calls);
const formattedContentArr = Array.isArray(formattedContent) ? formattedContent : [{
type: "text",
text: formattedContent
}];
const missingToolCalls = message.tool_calls.filter((toolCall) => !content.find((contentPart) => (contentPart.type === "tool_use" || contentPart.type === "input_json_delta" || contentPart.type === "server_tool_use") && contentPart.id === toolCall.id));
return {
role,
content: [...formattedContentArr, ...missingToolCalls.map(_convertLangChainToolCallToAnthropic)]
};
}
else return {
role,
content: _formatContent(message, AIMessage.isInstance(message) ? message.tool_calls : void 0)
};
})),
system
};
}
/**
* Applies cache_control to the last content block of the last message in the payload.
* This is the recommended approach for prompt caching as it applies the cache_control
* at the final formatting layer, after all message processing is complete.
*
* This matches the Python langchain-anthropic implementation where cache_control
* is applied via model_settings rather than modifying message content blocks directly.
*
* @param payload - The formatted Anthropic message payload
* @param cacheControl - The cache control configuration to apply
* @returns The payload with cache_control applied to the last content block
*/
function applyCacheControlToPayload(payload, cacheControl) {
if (!payload.messages || payload.messages.length === 0) return payload;
const messages = [...payload.messages];
const lastMessageIndex = messages.length - 1;
const lastMessage = messages[lastMessageIndex];
if (!lastMessage) return payload;
if (typeof lastMessage.content === "string") {
messages[lastMessageIndex] = {
...lastMessage,
content: [{
type: "text",
text: lastMessage.content,
cache_control: cacheControl
}]
};
return {
...payload,
messages
};
}
if (Array.isArray(lastMessage.content) && lastMessage.content.length > 0) {
const content = [...lastMessage.content];
const lastBlockIndex = content.length - 1;
content[lastBlockIndex] = {
...content[lastBlockIndex],
cache_control: cacheControl
};
messages[lastMessageIndex] = {
...lastMessage,
content
};
return {
...payload,
messages
};
}
return payload;
}
function mergeMessages(messages) {
if (!messages || messages.length <= 1) return messages;
const result = [];
let currentMessage = messages[0];
const normalizeContent = (content) => {
if (typeof content === "string") return [{
type: "text",
text: content
}];
return content;
};
const isToolResultMessage = (msg) => {
if (msg.role !== "user") return false;
if (typeof msg.content === "string") return false;
return Array.isArray(msg.content) && msg.content.every((item) => item.type === "tool_result");
};
for (let i = 1; i < messages.length; i += 1) {
const nextMessage = messages[i];
if (isToolResultMessage(currentMessage) && isToolResultMessage(nextMessage)) currentMessage = {
...currentMessage,
content: [...normalizeContent(currentMessage.content), ...normalizeContent(nextMessage.content)]
};
else {
result.push(currentMessage);
currentMessage = nextMessage;
}
}
result.push(currentMessage);
return result;
}
//#endregion
export { _convertMessagesToAnthropicPayload, applyCacheControlToPayload };
//# sourceMappingURL=message_inputs.js.map