diff --git a/.changeset/fix-extended-thinking-blocks.md b/.changeset/fix-extended-thinking-blocks.md new file mode 100644 index 0000000..52aa94c --- /dev/null +++ b/.changeset/fix-extended-thinking-blocks.md @@ -0,0 +1,5 @@ +--- +"@centralinc/browseragent": patch +--- + +Fix extended thinking 400 error by ensuring all assistant messages start with thinking blocks when thinkingBudget is enabled diff --git a/loop.ts b/loop.ts index d5c9240..6d6e088 100644 --- a/loop.ts +++ b/loop.ts @@ -17,6 +17,7 @@ import { injectPromptCaching, truncateMessageHistory, cleanMessageHistory, + ensureThinkingBlocksForExtendedThinking, PROMPT_CACHING_BETA_FLAG, } from "./utils/message-processing"; import { makeApiToolResult } from "./utils/tool-results"; @@ -219,6 +220,10 @@ ${capabilityDocs}`, // Clean message history to ensure tool_use and tool_result blocks are properly paired cleanMessageHistory(messages); + // Ensure all assistant messages have thinking blocks when extended thinking is enabled + // This prevents 400 errors from the API + ensureThinkingBlocksForExtendedThinking(messages, !!thinkingBudget); + if (onlyNMostRecentImages) { maybeFilterToNMostRecentImages( messages, diff --git a/utils/message-processing.ts b/utils/message-processing.ts index 7bee476..86b7c85 100644 --- a/utils/message-processing.ts +++ b/utils/message-processing.ts @@ -244,3 +244,50 @@ export function cleanMessageHistory(messages: BetaMessageParam[]): void { } } } + +/** + * Ensure all assistant messages start with thinking blocks when extended thinking is enabled + * This prevents the 400 error: "Expected `thinking` or `redacted_thinking`, but found `text`" + * + * When thinking is enabled, the API requires that every assistant message must start with + * a thinking or redacted_thinking block. This function filters out any assistant messages + * that don't meet this requirement. + * + * @param messages - Array of conversation messages + * @param thinkingEnabled - Whether extended thinking is enabled + */ +export function ensureThinkingBlocksForExtendedThinking( + messages: BetaMessageParam[], + thinkingEnabled: boolean, +): void { + if (!thinkingEnabled) { + return; + } + + // Filter out assistant messages that don't start with a thinking block + // Keep user messages as they don't need thinking blocks + const indicesToRemove: number[] = []; + + for (let i = 0; i < messages.length; i++) { + const message = messages[i]; + if (message?.role === "assistant" && Array.isArray(message.content)) { + const firstBlock = message.content[0]; + const hasThinkingBlock = + firstBlock && + typeof firstBlock === "object" && + (firstBlock.type === "thinking" || firstBlock.type === "redacted_thinking"); + + if (!hasThinkingBlock) { + indicesToRemove.push(i); + } + } + } + + // Remove messages in reverse order to maintain correct indices + for (let i = indicesToRemove.length - 1; i >= 0; i--) { + const index = indicesToRemove[i]; + if (index !== undefined) { + messages.splice(index, 1); + } + } +}