@@ -264,16 +264,86 @@ static object MakeFieldDefObject(JsonNode? node)
264264
265265 static List < MessageParam > ConvertMessages ( IEnumerable < Step > messages , bool allowThinkingBlocks )
266266 {
267- List < Step > mergedToolMessages = MergeToolMessages ( messages ) ;
267+ List < Step > mergedToolMessages = [ .. SwitchServerToolResponsesAsUser ( MergeToolMessages ( messages ) ) ] ;
268268 return [ .. mergedToolMessages . Select ( x => ToAnthropicMessage ( x , allowThinkingBlocks ) ) ] ;
269269
270- static List < Step > MergeToolMessages ( IEnumerable < Step > messages )
270+ static IEnumerable < Step > SwitchServerToolResponsesAsUser ( IEnumerable < Step > steps )
271+ {
272+ // Anthropic requires tool responses to be in user messages
273+ // When response like web_search_tool_response comes back from server, we need to switch them to user role from assistant
274+ // For example: [user, assistant(think, tool, tool_response, text), user] -> [user, assistant(think, tool), user(tool_response), assistant(text), user]
275+ foreach ( Step step in steps )
276+ {
277+ if ( step . ChatRole != DBChatRole . Assistant )
278+ {
279+ yield return step ;
280+ continue ;
281+ }
282+
283+ List < StepContent > assistantBuffer = new ( ) ;
284+ List < StepContent > userBuffer = new ( ) ;
285+
286+ foreach ( StepContent part in step . StepContents )
287+ {
288+ if ( part . StepContentToolCallResponse is not null )
289+ {
290+ // flush any accumulated assistant parts before emitting user tool responses
291+ if ( assistantBuffer . Count > 0 )
292+ {
293+ yield return new Step ( )
294+ {
295+ ChatRoleId = ( byte ) DBChatRole . Assistant ,
296+ StepContents = [ .. assistantBuffer ] ,
297+ } ;
298+ assistantBuffer . Clear ( ) ;
299+ }
300+
301+ userBuffer . Add ( part ) ;
302+ }
303+ else
304+ {
305+ // if we have accumulated user tool responses, emit them first
306+ if ( userBuffer . Count > 0 )
307+ {
308+ yield return new Step ( )
309+ {
310+ ChatRoleId = ( byte ) DBChatRole . User ,
311+ StepContents = [ .. userBuffer ] ,
312+ } ;
313+ userBuffer . Clear ( ) ;
314+ }
315+
316+ assistantBuffer . Add ( part ) ;
317+ }
318+ }
319+
320+ // flush remaining buffers in the original order
321+ if ( assistantBuffer . Count > 0 )
322+ {
323+ yield return new Step ( )
324+ {
325+ ChatRoleId = ( byte ) DBChatRole . Assistant ,
326+ StepContents = [ .. assistantBuffer ] ,
327+ } ;
328+ }
329+
330+ if ( userBuffer . Count > 0 )
331+ {
332+ yield return new Step ( )
333+ {
334+ ChatRoleId = ( byte ) DBChatRole . User ,
335+ StepContents = [ .. userBuffer ] ,
336+ } ;
337+ }
338+ }
339+ }
340+
341+ static IEnumerable < Step > MergeToolMessages ( IEnumerable < Step > messages )
271342 {
272343 // openai will omit tool messages, but anthropic needs them merged into the user message
273344 // for example:
274345 // openai: [user, assistant(request tool call, probably multiple), tool(tool response 1), tool(tool response 2), assistant]
275346 // anthropic: [user, assistant(request tool call, probably multiple), user(tool response 1 + 2), assistant]
276- List < Step > result = [ ] ;
277347 List < StepContent > toolBuffer = [ ] ;
278348
279349 foreach ( Step message in messages )
@@ -286,27 +356,25 @@ static List<Step> MergeToolMessages(IEnumerable<Step> messages)
286356 {
287357 if ( toolBuffer . Count > 0 )
288358 {
289- result . Add ( new Step ( )
359+ yield return new Step ( )
290360 {
291361 ChatRoleId = ( byte ) DBChatRole . User ,
292362 StepContents = [ .. toolBuffer ] ,
293- } ) ;
363+ } ;
294364 toolBuffer . Clear ( ) ;
295365 }
296- result . Add ( message ) ;
366+ yield return message ;
297367 }
298368 }
299369
300370 if ( toolBuffer . Count > 0 )
301371 {
302- result . Add ( new Step ( )
372+ yield return new Step ( )
303373 {
304374 ChatRoleId = ( byte ) DBChatRole . User ,
305375 StepContents = [ .. toolBuffer ] ,
306- } ) ;
376+ } ;
307377 }
308-
309- return result ;
310378 }
311379
312380 static MessageParam ToAnthropicMessage ( Step message , bool allowThinkingBlocks )
0 commit comments