Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fix-extended-thinking-blocks.md
Original file line number Diff line number Diff line change
@@ -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
5 changes: 5 additions & 0 deletions loop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
injectPromptCaching,
truncateMessageHistory,
cleanMessageHistory,
ensureThinkingBlocksForExtendedThinking,
PROMPT_CACHING_BETA_FLAG,
} from "./utils/message-processing";
import { makeApiToolResult } from "./utils/tool-results";
Expand Down Expand Up @@ -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,
Expand Down
47 changes: 47 additions & 0 deletions utils/message-processing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider using Array.filter to remove messages instead of collecting indices and splicing in reverse; also, the check for index !== undefined is redundant.

messages.splice(index, 1);
}
}
}