@@ -392,6 +392,18 @@ function injectCacheControlOnLastBlock(messages: MessageParam[]): void {
392392 }
393393}
394394
395+ /**
396+ * Whether a user MessageParam consists solely of `tool_result` blocks. Used to
397+ * keep tool results bundled with each other (parallel-tool-use spec) while
398+ * not merging a tool-result user message into an adjacent plain-text user
399+ * message — the two carry different semantics and must stay separate.
400+ */
401+ function isToolResultOnly ( message : MessageParam ) : boolean {
402+ if ( message . role !== 'user' ) return false ;
403+ const content = message . content ;
404+ if ( ! Array . isArray ( content ) || content . length === 0 ) return false ;
405+ return content . every ( ( block ) => block . type === 'tool_result' ) ;
406+ }
395407interface AnthropicImageBlock {
396408 type : 'image' ;
397409 source : { type : 'base64' ; data : string ; media_type : string } | { type : 'url' ; url : string } ;
@@ -983,15 +995,19 @@ export class AnthropicChatProvider implements ChatProvider {
983995 ]
984996 : undefined ;
985997
986- // Convert messages, merging consecutive user messages into one. Strict
987- // Anthropic-compatible backends reject consecutive user messages with HTTP
988- // 400 ("roles must alternate"), and api.anthropic.com concatenates them
989- // anyway — so merging is safe for native Anthropic and required for strict
990- // backends. This subsumes the parallel-tool-use requirement that all
991- // tool_result blocks answering parallel tool_use calls live in a single
992- // user message. Consecutive user messages arise naturally after compaction
993- // (kept user prompts + user-role summary + injected reminders) and from
994- // back-to-back system/tool messages converted to user role above.
998+ // Convert messages, merging consecutive user messages of the same kind into
999+ // one. Strict Anthropic-compatible backends reject consecutive user messages
1000+ // with HTTP 400 ("roles must alternate"), and api.anthropic.com concatenates
1001+ // them anyway — so merging is safe for native Anthropic and required for
1002+ // strict backends. Plain-text user messages merge with plain-text user
1003+ // messages; tool-result-only user messages merge with tool-result-only ones
1004+ // (the parallel-tool-use spec requires all tool_result blocks answering
1005+ // parallel tool_use calls to live in a single user message). A plain-text
1006+ // user message is intentionally NOT merged into an adjacent tool-result one:
1007+ // the two carry different semantics and must stay separate. Consecutive
1008+ // plain-text user messages arise naturally after compaction (kept user
1009+ // prompts + user-role summary + injected reminders) and from back-to-back
1010+ // system messages converted to user role above.
9951011 const messages : MessageParam [ ] = [ ] ;
9961012 const normalizedHistory = normalizeToolCallIdsForProvider (
9971013 history ,
@@ -1000,7 +1016,12 @@ export class AnthropicChatProvider implements ChatProvider {
10001016 for ( const msg of normalizedHistory ) {
10011017 const converted = convertMessage ( msg , this . _model ) ;
10021018 const last = messages . at ( - 1 ) ;
1003- if ( last !== undefined && last . role === 'user' && converted . role === 'user' ) {
1019+ if (
1020+ last !== undefined &&
1021+ last . role === 'user' &&
1022+ converted . role === 'user' &&
1023+ isToolResultOnly ( last ) === isToolResultOnly ( converted )
1024+ ) {
10041025 last . content = [
10051026 ...( last . content as ContentBlockParam [ ] ) ,
10061027 ...( converted . content as ContentBlockParam [ ] ) ,
0 commit comments