From 082b50d2c23436450743cbc75c042f8301a9e11b Mon Sep 17 00:00:00 2001 From: Amp Date: Wed, 16 Sep 2026 18:53:41 +0000 Subject: [PATCH 1/2] fix(api): preserve provider reasoning across tool calls Amp-Thread-ID: https://ampcode.com/threads/T-01a0aac1-c3f8-7220-a896-ac2dafbf72e1 --- src/api/providers/__tests__/minimax.spec.ts | 51 +++++++++++++++++ src/api/providers/minimax.ts | 12 +++- .../__tests__/bedrock-converse-format.spec.ts | 55 +++++++++++++++++++ src/api/transform/bedrock-converse-format.ts | 23 +++++++- 4 files changed, 139 insertions(+), 2 deletions(-) diff --git a/src/api/providers/__tests__/minimax.spec.ts b/src/api/providers/__tests__/minimax.spec.ts index 01102b0457..f1ed9dae0e 100644 --- a/src/api/providers/__tests__/minimax.spec.ts +++ b/src/api/providers/__tests__/minimax.spec.ts @@ -342,6 +342,57 @@ describe("MiniMaxHandler", () => { expect(firstChunk.value).toEqual({ type: "reasoning", text: thinkingContent }) }) + it("captures thinking signatures for the next tool-loop request", async () => { + mockCreate.mockResolvedValueOnce( + asyncStreamFrom([ + { + type: "content_block_delta", + index: 0, + delta: { type: "thinking_delta", thinking: "Inspect the file." }, + }, + { + type: "content_block_delta", + index: 0, + delta: { type: "signature_delta", signature: "signed-reasoning" }, + }, + ]), + ) + + const chunks = await collectStream(handler.createMessage("system prompt", [])) + + expect(chunks).toEqual([{ type: "reasoning", text: "Inspect the file." }]) + expect(handler.getThoughtSignature()).toBe("signed-reasoning") + }) + + it("filters legacy reasoning blocks while preserving signed thinking blocks", async () => { + mockCreate.mockResolvedValueOnce(asyncStreamFrom([])) + // The Anthropic SDK does not model Zoo Code's legacy internal reasoning block, + // which can still be present in persisted conversation history. + const messages = [ + { + role: "assistant", + content: [ + { type: "reasoning", text: "legacy unsigned reasoning", summary: [] }, + { type: "thinking", thinking: "signed reasoning", signature: "signature" }, + { type: "text", text: "I will inspect the file." }, + ], + }, + ] as unknown as Anthropic.Messages.MessageParam[] + + await collectStream(handler.createMessage("system prompt", messages)) + + const request = mockCreate.mock.calls[0][0] as Anthropic.Messages.MessageCreateParams + expect(request.messages).toEqual([ + { + role: "assistant", + content: [ + { type: "thinking", thinking: "signed reasoning", signature: "signature" }, + { type: "text", text: "I will inspect the file." }, + ], + }, + ]) + }) + it("should handle tool calls in stream", async () => { mockCreate.mockResolvedValueOnce( asyncStreamFrom([ diff --git a/src/api/providers/minimax.ts b/src/api/providers/minimax.ts index e209add72d..3abd2c8686 100644 --- a/src/api/providers/minimax.ts +++ b/src/api/providers/minimax.ts @@ -10,6 +10,7 @@ import type { ApiHandlerOptions } from "../../shared/api" import { ApiStream } from "../transform/stream" import { getModelParams } from "../transform/model-params" import { mergeEnvironmentDetailsForMiniMax } from "../transform/minimax-format" +import { filterNonAnthropicBlocks } from "../transform/anthropic-filter" import { BaseProvider } from "./base-provider" import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata, CompletePromptOptions } from "../index" @@ -53,6 +54,7 @@ function convertOpenAIToolChoice( export class MiniMaxHandler extends BaseProvider implements SingleCompletionHandler { private options: ApiHandlerOptions private client: Anthropic + private lastThoughtSignature?: string constructor(options: ApiHandlerOptions) { super() @@ -84,6 +86,7 @@ export class MiniMaxHandler extends BaseProvider implements SingleCompletionHand ): ApiStream { const cacheControl: CacheControlEphemeral = { type: "ephemeral" } const { id: modelId, info, maxTokens, temperature } = this.getModel() + this.lastThoughtSignature = undefined // MiniMax M2 models support prompt caching const supportsPromptCache = info.supportsPromptCache ?? false @@ -92,7 +95,7 @@ export class MiniMaxHandler extends BaseProvider implements SingleCompletionHand // into the tool_result content. This preserves reasoning continuity for // thinking models by preventing user messages from interrupting the // reasoning context after tool use (similar to r1-format's mergeToolResultText). - const processedMessages = mergeEnvironmentDetailsForMiniMax(messages) + const processedMessages = filterNonAnthropicBlocks(mergeEnvironmentDetailsForMiniMax(messages)) // Build the system blocks array const systemBlocks: Anthropic.Messages.TextBlockParam[] = [ @@ -194,6 +197,9 @@ export class MiniMaxHandler extends BaseProvider implements SingleCompletionHand case "thinking_delta": yield { type: "reasoning", text: chunk.delta.thinking } break + case "signature_delta": + this.lastThoughtSignature = chunk.delta.signature + break case "text_delta": yield { type: "text", text: chunk.delta.text } break @@ -236,6 +242,10 @@ export class MiniMaxHandler extends BaseProvider implements SingleCompletionHand } } + getThoughtSignature(): string | undefined { + return this.lastThoughtSignature + } + /** * Add cache control to the last two user messages for prompt caching */ diff --git a/src/api/transform/__tests__/bedrock-converse-format.spec.ts b/src/api/transform/__tests__/bedrock-converse-format.spec.ts index 27319c6562..77dced1351 100644 --- a/src/api/transform/__tests__/bedrock-converse-format.spec.ts +++ b/src/api/transform/__tests__/bedrock-converse-format.spec.ts @@ -26,6 +26,61 @@ describe("convertToBedrockConverseMessages", () => { ]) }) + it("converts internal reasoning blocks to Bedrock reasoning content", () => { + // The Anthropic SDK does not model Zoo Code's internal reasoning block, + // though this converter receives it from persisted conversation history. + const messages = [ + { + role: "assistant", + content: [{ type: "reasoning", text: "I should inspect the file first.", summary: [] }], + }, + ] as unknown as Anthropic.Messages.MessageParam[] + + expect(convertToBedrockConverseMessages(messages)).toEqual([ + { + role: "assistant", + content: [ + { + reasoningContent: { + reasoningText: { text: "I should inspect the file first." }, + }, + }, + ], + }, + ]) + }) + + it("converts signed thinking blocks to Bedrock reasoning content", () => { + const messages: Anthropic.Messages.MessageParam[] = [ + { + role: "assistant", + content: [ + { + type: "thinking", + thinking: "I should inspect the file first.", + signature: "signed-reasoning", + }, + ], + }, + ] + + expect(convertToBedrockConverseMessages(messages)).toEqual([ + { + role: "assistant", + content: [ + { + reasoningContent: { + reasoningText: { + text: "I should inspect the file first.", + signature: "signed-reasoning", + }, + }, + }, + ], + }, + ]) + }) + it("converts messages with images correctly", () => { const messages: Anthropic.Messages.MessageParam[] = [ { diff --git a/src/api/transform/bedrock-converse-format.ts b/src/api/transform/bedrock-converse-format.ts index 2a49d72bce..fab2769a7e 100644 --- a/src/api/transform/bedrock-converse-format.ts +++ b/src/api/transform/bedrock-converse-format.ts @@ -3,8 +3,10 @@ import { ConversationRole, Message, ContentBlock } from "@aws-sdk/client-bedrock import { sanitizeOpenAiCallId } from "../../utils/tool-id" interface BedrockMessageContent { - type: "text" | "image" | "video" | "tool_use" | "tool_result" + type: "text" | "image" | "video" | "tool_use" | "tool_result" | "reasoning" | "thinking" text?: string + thinking?: string + signature?: string source?: { type: "base64" data: string | Uint8Array // string for Anthropic, Uint8Array for Bedrock @@ -58,6 +60,25 @@ export function convertToBedrockConverseMessages(anthropicMessages: Anthropic.Me } as ContentBlock } + if (messageBlock.type === "reasoning" && typeof messageBlock.text === "string") { + return { + reasoningContent: { + reasoningText: { text: messageBlock.text }, + }, + } as ContentBlock + } + + if (messageBlock.type === "thinking" && typeof messageBlock.thinking === "string") { + return { + reasoningContent: { + reasoningText: { + text: messageBlock.thinking, + ...(messageBlock.signature ? { signature: messageBlock.signature } : {}), + }, + }, + } as ContentBlock + } + if (messageBlock.type === "image" && messageBlock.source) { // Convert base64 string to byte array if needed let byteArray: Uint8Array From 8c366e7fa8fbd5d0b957937328287ac843ce9db3 Mon Sep 17 00:00:00 2001 From: PierrunoYT Date: Fri, 18 Sep 2026 15:17:53 +0200 Subject: [PATCH 2/2] test(api): cover reasoning signature edge cases --- src/api/providers/__tests__/minimax.spec.ts | 28 +++++++++++++++++++ .../__tests__/bedrock-converse-format.spec.ts | 23 +++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/src/api/providers/__tests__/minimax.spec.ts b/src/api/providers/__tests__/minimax.spec.ts index f1ed9dae0e..051b3f5026 100644 --- a/src/api/providers/__tests__/minimax.spec.ts +++ b/src/api/providers/__tests__/minimax.spec.ts @@ -364,6 +364,34 @@ describe("MiniMaxHandler", () => { expect(handler.getThoughtSignature()).toBe("signed-reasoning") }) + it("clears stale thinking signatures before the next request", async () => { + mockCreate + .mockResolvedValueOnce( + asyncStreamFrom([ + { + type: "content_block_delta", + index: 0, + delta: { type: "signature_delta", signature: "signed-reasoning" }, + }, + ]), + ) + .mockResolvedValueOnce( + asyncStreamFrom([ + { + type: "content_block_delta", + index: 0, + delta: { type: "thinking_delta", thinking: "Continue without a signature." }, + }, + ]), + ) + + await collectStream(handler.createMessage("system prompt", [])) + expect(handler.getThoughtSignature()).toBe("signed-reasoning") + + await collectStream(handler.createMessage("system prompt", [])) + expect(handler.getThoughtSignature()).toBeUndefined() + }) + it("filters legacy reasoning blocks while preserving signed thinking blocks", async () => { mockCreate.mockResolvedValueOnce(asyncStreamFrom([])) // The Anthropic SDK does not model Zoo Code's legacy internal reasoning block, diff --git a/src/api/transform/__tests__/bedrock-converse-format.spec.ts b/src/api/transform/__tests__/bedrock-converse-format.spec.ts index 77dced1351..9d4a507e5c 100644 --- a/src/api/transform/__tests__/bedrock-converse-format.spec.ts +++ b/src/api/transform/__tests__/bedrock-converse-format.spec.ts @@ -81,6 +81,29 @@ describe("convertToBedrockConverseMessages", () => { ]) }) + it("converts unsigned thinking blocks without adding a signature", () => { + // Persisted provider output can omit a signature even though the Anthropic SDK requires one. + const messages = [ + { + role: "assistant", + content: [{ type: "thinking", thinking: "I should inspect the file first." }], + }, + ] as unknown as Anthropic.Messages.MessageParam[] + + expect(convertToBedrockConverseMessages(messages)).toStrictEqual([ + { + role: "assistant", + content: [ + { + reasoningContent: { + reasoningText: { text: "I should inspect the file first." }, + }, + }, + ], + }, + ]) + }) + it("converts messages with images correctly", () => { const messages: Anthropic.Messages.MessageParam[] = [ {