mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-09 22:31:08 +00:00
fix: handle tool_result blocks in Anthropic countTokens method
Fixes #9871 When the Anthropic countTokens method receives content containing tool_result blocks, it now falls back to local tiktoken estimation instead of making an API call. This prevents 400 errors because Anthropic's countTokens API validates message structure the same way as the chat API - tool_result blocks require a preceding assistant message with a matching tool_use block. Changes: - Added tool_result detection in AnthropicHandler.countTokens() - Added 5 test cases covering the new behavior
This commit is contained in:
parent
9f4dcfc0e6
commit
214790fc56
2 changed files with 101 additions and 0 deletions
|
|
@ -4,6 +4,7 @@ import { AnthropicHandler } from "../anthropic"
|
|||
import { ApiHandlerOptions } from "../../../shared/api"
|
||||
|
||||
const mockCreate = vitest.fn()
|
||||
const mockCountTokens = vitest.fn()
|
||||
|
||||
vitest.mock("@anthropic-ai/sdk", () => {
|
||||
const mockAnthropicConstructor = vitest.fn().mockImplementation(() => ({
|
||||
|
|
@ -52,6 +53,7 @@ vitest.mock("@anthropic-ai/sdk", () => {
|
|||
},
|
||||
}
|
||||
}),
|
||||
countTokens: mockCountTokens.mockResolvedValue({ input_tokens: 42 }),
|
||||
},
|
||||
}))
|
||||
|
||||
|
|
@ -60,6 +62,11 @@ vitest.mock("@anthropic-ai/sdk", () => {
|
|||
}
|
||||
})
|
||||
|
||||
// Mock the base countTokens utility used by BaseProvider fallback
|
||||
vitest.mock("../../../utils/countTokens", () => ({
|
||||
countTokens: vitest.fn().mockResolvedValue(15),
|
||||
}))
|
||||
|
||||
// Import after mock
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
|
||||
|
|
@ -727,4 +734,87 @@ describe("AnthropicHandler", () => {
|
|||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("countTokens", () => {
|
||||
beforeEach(() => {
|
||||
mockCountTokens.mockClear()
|
||||
})
|
||||
|
||||
it("should use Anthropic API for regular text content", async () => {
|
||||
const content = [{ type: "text" as const, text: "Hello, world!" }]
|
||||
|
||||
const result = await handler.countTokens(content)
|
||||
|
||||
expect(result).toBe(42)
|
||||
expect(mockCountTokens).toHaveBeenCalledTimes(1)
|
||||
expect(mockCountTokens).toHaveBeenCalledWith({
|
||||
model: "claude-3-5-sonnet-20241022",
|
||||
messages: [{ role: "user", content }],
|
||||
})
|
||||
})
|
||||
|
||||
it("should fall back to local estimation when content contains tool_result blocks", async () => {
|
||||
const content = [
|
||||
{
|
||||
type: "tool_result" as const,
|
||||
tool_use_id: "toolu_123",
|
||||
content: "File contents here",
|
||||
},
|
||||
]
|
||||
|
||||
const result = await handler.countTokens(content)
|
||||
|
||||
// Should return the fallback value (15) from the mocked countTokens utility
|
||||
expect(result).toBe(15)
|
||||
// Should NOT call the Anthropic API
|
||||
expect(mockCountTokens).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should fall back when content has mixed blocks including tool_result", async () => {
|
||||
const content = [
|
||||
{ type: "text" as const, text: "Some text" },
|
||||
{
|
||||
type: "tool_result" as const,
|
||||
tool_use_id: "toolu_456",
|
||||
content: "Tool output",
|
||||
},
|
||||
]
|
||||
|
||||
const result = await handler.countTokens(content)
|
||||
|
||||
// Should return the fallback value
|
||||
expect(result).toBe(15)
|
||||
// Should NOT call the Anthropic API
|
||||
expect(mockCountTokens).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should fall back to local estimation when API call fails", async () => {
|
||||
mockCountTokens.mockRejectedValueOnce(new Error("API Error"))
|
||||
|
||||
const content = [{ type: "text" as const, text: "Hello, world!" }]
|
||||
|
||||
const result = await handler.countTokens(content)
|
||||
|
||||
// Should return the fallback value
|
||||
expect(result).toBe(15)
|
||||
})
|
||||
|
||||
it("should handle image blocks via API", async () => {
|
||||
const content = [
|
||||
{
|
||||
type: "image" as const,
|
||||
source: {
|
||||
type: "base64" as const,
|
||||
media_type: "image/png" as const,
|
||||
data: "base64data",
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
const result = await handler.countTokens(content)
|
||||
|
||||
expect(result).toBe(42)
|
||||
expect(mockCountTokens).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -410,6 +410,17 @@ export class AnthropicHandler extends BaseProvider implements SingleCompletionHa
|
|||
* @returns A promise resolving to the token count
|
||||
*/
|
||||
override async countTokens(content: Array<Anthropic.Messages.ContentBlockParam>): Promise<number> {
|
||||
// Check if content contains tool_result blocks - these cannot be sent as standalone
|
||||
// user messages because Anthropic requires tool_result to follow a tool_use message
|
||||
// from the assistant. Fall back to local estimation in this case.
|
||||
const hasToolResult = content.some(
|
||||
(block) => typeof block === "object" && "type" in block && block.type === "tool_result",
|
||||
)
|
||||
|
||||
if (hasToolResult) {
|
||||
return super.countTokens(content)
|
||||
}
|
||||
|
||||
try {
|
||||
// Use the current model
|
||||
const { id: model } = this.getModel()
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue