Skip to content

Commit a62914c

Browse files
committed
fix(anthropic): Work around Anthropic 500 bug
Anthropic has a bug where if we pass them back one of their bash_code_execution_output blocks which contains a file output, they return a 500. I have reported the error to them, but in the meantime this works around the problem by dropping the problematic content blocks before we send them to anthropic Also adds an integration test that exercises this PR as well as: - langchain-ai#9108 - langchain-ai#9109
1 parent d1eddaf commit a62914c

File tree

3 files changed

+142
-35
lines changed

3 files changed

+142
-35
lines changed

libs/providers/langchain-anthropic/src/tests/chat_models-code_execution.int.test.ts

Lines changed: 78 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,36 @@
1-
import { describe, it, expect } from "vitest";
1+
import { describe, it, expect, beforeEach } from "vitest";
22
import Anthropic from "@anthropic-ai/sdk";
33
import { HumanMessage, AIMessage } from "@langchain/core/messages";
44
import { ChatAnthropic } from "../chat_models.js";
5+
import { extractGeneratedFiles } from "../utils/extractGeneratedFiles.js";
56
import fs from "fs";
67
import path from "path";
78
import os from "os";
89

910
describe("Files API with Code Execution", () => {
11+
let model: ReturnType<ChatAnthropic["bindTools"]>;
12+
13+
beforeEach(() => {
14+
// Create ChatAnthropic model with code execution beta header and bind tools
15+
const baseModel = new ChatAnthropic({
16+
model: "claude-3-5-haiku-20241022",
17+
temperature: 0,
18+
clientOptions: {
19+
defaultHeaders: {
20+
"anthropic-beta": "code-execution-2025-08-25,files-api-2025-04-14",
21+
},
22+
},
23+
});
24+
25+
// Bind the code_execution tool
26+
model = baseModel.bindTools([
27+
{
28+
type: "code_execution_20250825" as const,
29+
name: "code_execution",
30+
},
31+
]);
32+
});
33+
1034
it("should handle createMessageWithFiles using container_upload blocks", async () => {
1135
// Create Anthropic client for Files API
1236
const anthropicClient = new Anthropic({
@@ -26,23 +50,6 @@ describe("Files API with Code Execution", () => {
2650
file: fs.createReadStream(tmpFilePath),
2751
});
2852

29-
// Create ChatAnthropic model with code execution beta header
30-
const model = new ChatAnthropic({
31-
model: "claude-3-5-haiku-20241022",
32-
temperature: 0,
33-
clientOptions: {
34-
defaultHeaders: {
35-
"anthropic-beta": "code-execution-2025-08-25,files-api-2025-04-14",
36-
},
37-
},
38-
});
39-
40-
// Define the built-in code_execution tool
41-
const codeExecutionTool = {
42-
type: "code_execution_20250825" as const,
43-
name: "code_execution",
44-
};
45-
4653
// Create a message with container_upload block
4754
const message = new HumanMessage({
4855
content: [
@@ -55,9 +62,7 @@ describe("Files API with Code Execution", () => {
5562
});
5663

5764
// Invoke the model with the file
58-
const result = await model.invoke([message], {
59-
tools: [codeExecutionTool],
60-
});
65+
const result = await model.invoke([message]);
6166

6267
// Verify the result is an AIMessage
6368
expect(result).toBeInstanceOf(AIMessage);
@@ -86,26 +91,64 @@ describe("Files API with Code Execution", () => {
8691
)
8792
).toBe(true);
8893

89-
// The response should contain text with the calculated average
90-
const textBlocks = content.filter(
91-
(block) =>
92-
typeof block === "object" && "type" in block && block.type === "text"
93-
);
94-
const responseText = textBlocks
95-
.map((block) =>
96-
typeof block === "object" && "text" in block ? block.text : ""
97-
)
98-
.join(" ")
99-
.toLowerCase();
100-
10194
// The response should mention the average age (30)
102-
expect(responseText).toMatch(/average|mean/i);
103-
expect(responseText).toMatch(/30/);
95+
expect(result.text).toMatch(/average|mean/i);
96+
expect(result.text).toMatch(/30/);
10497
} finally {
10598
// Clean up temporary file
10699
if (fs.existsSync(tmpFilePath)) {
107100
fs.unlinkSync(tmpFilePath);
108101
}
109102
}
110103
}, 60000);
104+
105+
it("should pass container and file outputs across multiple turns", async () => {
106+
// First invocation: Calculate mean and avg, store to file
107+
const firstMessage = new HumanMessage(
108+
"Calculate the mean and average of these numbers: 10, 20, 30, 40, 50. Store the results in a file called 'results.txt'."
109+
);
110+
111+
const firstResult = await model.invoke([firstMessage]);
112+
113+
// Verify first result succeeded
114+
expect(firstResult).toBeInstanceOf(AIMessage);
115+
116+
// Extract container ID from first response
117+
const container = (firstResult as AIMessage).additional_kwargs
118+
?.container as { id: string; expires_at: string } | undefined;
119+
expect(container?.id).toBeTruthy();
120+
121+
// Verify file output was created using extractGeneratedFilesAnthropic
122+
const fileIds = extractGeneratedFiles(
123+
firstResult as unknown as Anthropic.Beta.BetaMessage
124+
);
125+
expect(fileIds.length).toBeGreaterThan(0);
126+
127+
// Second invocation: Read the file with same container
128+
// This should succeed because we apply the workaround in message_inputs.ts
129+
const secondMessage = new HumanMessage(
130+
"What are the contents of the results.txt file?"
131+
);
132+
133+
const secondResult = await model.invoke(
134+
[firstMessage, firstResult, secondMessage],
135+
{
136+
container: container?.id, // Pass container to reuse files
137+
}
138+
);
139+
140+
// Verify second result succeeded
141+
expect(secondResult).toBeInstanceOf(AIMessage);
142+
const secondContent = Array.isArray(secondResult.content)
143+
? secondResult.content
144+
: [];
145+
expect(secondContent.length).toBeGreaterThan(0);
146+
147+
// Verify the same container was reused
148+
const secondContainer = (secondResult as AIMessage).additional_kwargs
149+
?.container as { id: string; expires_at: string } | undefined;
150+
expect(secondContainer?.id).toBe(container?.id);
151+
152+
expect(secondResult.text).toMatch(/30/);
153+
}, 60000);
111154
});
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import type Anthropic from "@anthropic-ai/sdk";
2+
3+
/**
4+
* Extract generated file IDs from an Anthropic code execution response.
5+
*
6+
* Parses the response content blocks to find files created during code execution.
7+
* Note: This function only returns file IDs.
8+
*
9+
* @param message - The Anthropic message response
10+
* @returns Array of file IDs
11+
*/
12+
export function extractGeneratedFiles(
13+
message: Anthropic.Beta.BetaMessage
14+
): string[] {
15+
const fileIds: string[] = [];
16+
17+
for (const item of message.content) {
18+
if (
19+
item.type === "bash_code_execution_tool_result" &&
20+
item.content.type === "bash_code_execution_result"
21+
) {
22+
for (const file of item.content.content) {
23+
if (file.type === "bash_code_execution_output") {
24+
fileIds.push(file.file_id);
25+
}
26+
}
27+
}
28+
}
29+
30+
return fileIds;
31+
}

libs/providers/langchain-anthropic/src/utils/message_inputs.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,36 @@ function _formatImage(imageUrl: string) {
8181
);
8282
}
8383

84+
/**
85+
* Work around Anthropic API bug where bash_code_execution_output blocks
86+
* cause 500 errors when included in conversation history.
87+
* Filters out bash_code_execution_output blocks from bash_code_execution_tool_result blocks.
88+
*
89+
* @param contentPart The content block to potentially filter
90+
*/
91+
function _filterBashCodeExecutionOutputs(contentPart: { type: string }): void {
92+
if (contentPart.type !== "bash_code_execution_tool_result") {
93+
return;
94+
}
95+
96+
const toolResult =
97+
contentPart as unknown as Anthropic.Beta.Messages.BetaBashCodeExecutionToolResultBlockParam;
98+
99+
if (
100+
toolResult.content.type === "bash_code_execution_result" &&
101+
Array.isArray(toolResult.content.content)
102+
) {
103+
// Filter out bash_code_execution_output blocks
104+
const filteredContent = toolResult.content.content.filter(
105+
(c) => c.type !== "bash_code_execution_output"
106+
);
107+
toolResult.content = {
108+
...toolResult.content,
109+
content: filteredContent,
110+
};
111+
}
112+
}
113+
84114
function _ensureMessageContents(messages: BaseMessage[]): BaseMessage[] {
85115
// Merge runs of human/tool messages into single human messages with content blocks.
86116
const updatedMsgs = [];
@@ -268,6 +298,9 @@ function* _formatContentBlocks(
268298
}
269299
}
270300
}
301+
302+
_filterBashCodeExecutionOutputs(contentPartCopy);
303+
271304
// TODO: Fix when SDK types are fixed
272305
yield {
273306
...contentPartCopy,

0 commit comments

Comments
 (0)