From 8b9ae4bb5891aa72f1f9964613ed21abd08f2c92 Mon Sep 17 00:00:00 2001 From: Roo Code Date: Thu, 12 Feb 2026 04:10:48 +0000 Subject: [PATCH] fix: apply reasoning stripping to anthropic-vertex, add cache control test - Apply the same reasoning_details/reasoning_content stripping to anthropic-vertex.ts that was already applied to anthropic.ts and openrouter.ts (identical no-op cast bug) - Pass aiSdkMessages (mapped copy) to applyCacheControlToAiSdkMessages in anthropic-vertex.ts so cache control mutations target the copies sent to streamText - Add test verifying cache control providerOptions are applied to the mapped message copies (not the originals) when messages contain extra legacy fields --- src/api/providers/__tests__/anthropic.spec.ts | 41 +++++++++++++++++++ src/api/providers/anthropic-vertex.ts | 10 +++-- 2 files changed, 48 insertions(+), 3 deletions(-) diff --git a/src/api/providers/__tests__/anthropic.spec.ts b/src/api/providers/__tests__/anthropic.spec.ts index 331c210bcd..ab463e4fd7 100644 --- a/src/api/providers/__tests__/anthropic.spec.ts +++ b/src/api/providers/__tests__/anthropic.spec.ts @@ -438,6 +438,47 @@ describe("AnthropicHandler", () => { expect(callArgs.messages[1].content).toEqual([{ type: "text", text: "Hi" }]) }) + it("should apply cache control providerOptions to mapped message copies (not originals)", async () => { + setupStreamTextMock([{ type: "text-delta", text: "test" }]) + + // Messages with extra legacy fields — .map() creates new objects to strip them + const messagesWithExtraFields = [ + { + role: "user", + content: [{ type: "text" as const, text: "Hello" }], + }, + { + role: "assistant", + content: [{ type: "text" as const, text: "Hi" }], + reasoning_details: [{ type: "thinking", thinking: "deep thoughts" }], + }, + { + role: "user", + content: [{ type: "text" as const, text: "Follow up" }], + }, + ] as any + + const stream = handler.createMessage(systemPrompt, messagesWithExtraFields) + + for await (const _chunk of stream) { + // Consume stream + } + + expect(mockStreamText).toHaveBeenCalledTimes(1) + const callArgs = mockStreamText.mock.calls[0]![0] + + // The last user message (index 2) should have cache control applied + const lastUserMsg = callArgs.messages[2] + expect(lastUserMsg.role).toBe("user") + expect(lastUserMsg.providerOptions).toBeDefined() + expect(lastUserMsg.providerOptions.anthropic).toEqual({ + cacheControl: { type: "ephemeral" }, + }) + + // And it should still NOT have reasoning_details (stripped by .map()) + expect(lastUserMsg).not.toHaveProperty("reasoning_details") + }) + it("should pass system prompt via system param with systemProviderOptions for cache control", async () => { setupStreamTextMock([{ type: "text-delta", text: "test" }]) diff --git a/src/api/providers/anthropic-vertex.ts b/src/api/providers/anthropic-vertex.ts index 131b36992e..bf10aff162 100644 --- a/src/api/providers/anthropic-vertex.ts +++ b/src/api/providers/anthropic-vertex.ts @@ -90,8 +90,12 @@ export class AnthropicVertexHandler extends BaseProvider implements SingleComple ): ApiStream { const modelConfig = this.getModel() - // Convert messages to AI SDK format - const aiSdkMessages = messages as ModelMessage[] + // Convert messages to AI SDK format, stripping extra fields from legacy + // ApiMessage objects that survive JSON deserialization (e.g. reasoning_details + // causes Anthropic 400: "Extra inputs are not permitted"). + const aiSdkMessages = messages.map( + ({ reasoning_details, reasoning_content, ...rest }: any) => rest, + ) as ModelMessage[] // Convert tools to AI SDK format const openAiTools = this.convertToolsForOpenAI(metadata?.tools) @@ -147,7 +151,7 @@ export class AnthropicVertexHandler extends BaseProvider implements SingleComple if (secondLastUserMsgIndex >= 0) targetIndices.add(secondLastUserMsgIndex) if (targetIndices.size > 0) { - this.applyCacheControlToAiSdkMessages(messages as ModelMessage[], targetIndices, cacheProviderOption) + this.applyCacheControlToAiSdkMessages(aiSdkMessages, targetIndices, cacheProviderOption) } // Build streamText request