Skip to main content

Fix Semantic Kernel Tool Call Message Ordering Error

From GitHub Issue .NET 9 Microsoft.SemanticKernel 1.54.0 Azure.AI.OpenAI 2.1.0
By Rajesh Mishra · Mar 21, 2026 · 8 min read
Verified Mar 2026 .NET 9 Microsoft.SemanticKernel 1.54.0
In 30 Seconds

The SK 400 model_error 'tool_calls must be followed by tool messages' occurs when ChatHistory has a message between an assistant's tool call and its tool result. Workarounds: (1) post-process ChatHistory to fix ordering violations, (2) use MEAI's FunctionInvokingChatClient which handles ordering correctly, or (3) disable auto-invoke and manage the function call loop manually.

⚠️
Error Fix Guide

Root cause analysis and verified fix. Code examples use Microsoft.SemanticKernel 1.54.0.

✓ SOLVED

The Error

Your Semantic Kernel application fails with this exception during a multi-turn conversation:

Azure.RequestFailedException: 400 (Bad Request)
{
  "error": {
    "code": "model_error",
    "message": "tool_calls must be followed by tool messages"
  }
}

This is not a transient error — it is a message ordering violation in your ChatHistory. Azure OpenAI enforces a strict sequence requirement: every assistant message that contains a tool call must be immediately followed by a tool result message. Any other message in between produces this 400.

When It Triggers

The error surfaces in multi-turn conversations that use FunctionChoiceBehavior.Auto(). Here is the typical sequence that breaks:

  1. User sends message — SK calls Azure OpenAI
  2. Model responds with a tool call — SK adds the assistant message to ChatHistory
  3. SK executes the function and adds the tool result — ChatHistory now has the correct pairing
  4. User sends a follow-up message before the conversation continues — SK adds this new user message to ChatHistory
  5. SK calls Azure OpenAI again — ChatHistory now has a user message sitting after an old tool call → tool result block that was not properly closed out in a previous turn

The history is malformed from Azure OpenAI’s perspective, even though the SK auto-invoke appeared to handle it correctly in the moment. The corruption accumulates over multiple turns.

This is tracked as SK Issue #7626 on the Semantic Kernel GitHub repository.

Root Cause: Message Ordering Requirements

Azure OpenAI enforces a strict message sequence for function calling conversations. The required order is:

Turn 1: User → "What's the weather in London?"
Turn 2: Assistant → [tool_call: get_weather(city="London")]
Turn 3: Tool → [result: "15°C, partly cloudy"]
Turn 4: Assistant → "The weather in London is 15°C..."

Visualized as roles in ChatHistory:

User → Assistant (with tool call) → Tool (result) → Assistant (final answer) → User → ...

The constraint is: a Tool message must immediately follow the Assistant message that produced the tool call. If anything else — another User message, a System message, another Assistant message — appears between them, the 400 error fires.

SK’s auto-invoke path can violate this constraint in complex multi-turn scenarios, particularly when user input arrives asynchronously relative to function execution.

Workaround 1: Manual Function Call Loop

The most reliable fix is to disable SK’s auto-invoke and handle the function call loop yourself. This gives you direct control over message ordering:

using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.ChatCompletion;
using Microsoft.SemanticKernel.Connectors.OpenAI;

var chatHistory = new ChatHistory("You are a helpful assistant.");
chatHistory.AddUserMessage(userMessage);

var settings = new OpenAIPromptExecutionSettings
{
    // Disable auto-invoke — we'll handle function calls manually
    FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(autoInvoke: false)
};

var chatService = kernel.GetRequiredService<IChatCompletionService>();

while (true)
{
    var response = await chatService.GetChatMessageContentAsync(
        chatHistory, settings, kernel);

    chatHistory.Add(response);

    // Check if the model wants to call a function
    if (response.Content?.Contains("tool_calls") == true ||
        response.Items?.OfType<FunctionCallContent>().Any() == true)
    {
        // Process each function call
        foreach (var call in response.Items?.OfType<FunctionCallContent>() ?? [])
        {
            var result = await kernel.InvokeAsync(
                call.PluginName, call.FunctionName, call.Arguments);

            // Add tool result immediately after the assistant message
            chatHistory.Add(new ChatMessageContent(AuthorRole.Tool,
                result.GetValue<string>() ?? string.Empty));
        }
        // Continue the loop to get the final response
    }
    else
    {
        // Model gave a final text response — we're done
        Console.WriteLine(response.Content);
        break;
    }
}

The key discipline here: tool result messages are added to ChatHistory immediately after processing each function call, before any user input is accepted. The loop structure enforces the correct ordering invariant.

For a deeper look at how function calling and tool use works with Semantic Kernel, including plugin registration and argument handling, see the university guide.

Workaround 2: FunctionInvokingChatClient from MEAI

Microsoft.Extensions.AI provides FunctionInvokingChatClient, a middleware layer that manages the function calling loop — including correct message ordering — independent of SK’s auto-invoke path:

using Microsoft.Extensions.AI;

// Register with FunctionInvokingChatClient middleware
builder.Services.AddAzureOpenAIChatClient(
    new Uri(endpoint), new AzureKeyCredential(apiKey))
    .UseFunctionInvocation(); // Handles tool call ↔ tool result ordering correctly

// Register kernel functions as tools
builder.Services.AddChatOptions(options =>
{
    options.Tools = [AIFunctionFactory.Create(MyPlugin.GetWeather)];
});

FunctionInvokingChatClient wraps the underlying IChatClient and intercepts responses that contain tool calls, executes them, appends the results with correct message ordering, and continues the conversation — all before returning to your application code. The ordering bug that affects SK’s auto-invoke path does not occur because MEAI controls the entire message sequence.

For a comparison of MEAI’s abstractions versus Semantic Kernel’s approach and when to use each, see Microsoft.Extensions.AI vs Semantic Kernel vs Agent Framework.

Workaround 3: ChatHistory Validation and Repair

If you cannot change the function calling approach immediately, add a validation pass before every multi-turn AI request to detect and remove orphaned tool call messages:

public static void ValidateAndRepairToolCallOrdering(ChatHistory history)
{
    for (int i = 0; i < history.Count - 1; i++)
    {
        var current = history[i];
        var next = history[i + 1];

        bool currentHasToolCall = current.Role == AuthorRole.Assistant &&
            current.Items?.OfType<FunctionCallContent>().Any() == true;
        bool nextIsNotTool = next.Role != AuthorRole.Tool;

        if (currentHasToolCall && nextIsNotTool)
        {
            // Remove the orphaned tool call to prevent the 400 error
            history.RemoveAt(i);
            i--; // Recheck current position
        }
    }
}

// Call before every multi-turn AI request
ValidateAndRepairToolCallOrdering(chatHistory);

This approach removes the malformed assistant-with-tool-call message, which means the conversation loses that turn’s context. The result is a valid ChatHistory that Azure OpenAI can process without errors, at the cost of that tool call’s exchange being dropped from context. This is a mitigation, not a fix — use it as a safety net while migrating to Workaround 1 or 2.

Diagnosing the Error in Production

Because this error is intermittent and depends on the exact sequence of operations in a multi-turn conversation, it is difficult to reproduce in unit tests. Add structured logging of ChatHistory message roles before each API call:

_logger.LogDebug(
    "ChatHistory roles before API call: {Roles}",
    string.Join(" → ", chatHistory.Select(m => m.Role.ToString())));

When the 400 error occurs, the log entry immediately before it will show the role sequence that caused the violation. Look for Assistant → User or Assistant → Assistant patterns following an assistant message — those indicate an ordering violation.

Tracking the Fix

SK Issue #7626 is an open issue in the Semantic Kernel repository. Recent releases (1.50.0+) have included partial fixes for specific ordering scenarios, but the issue can still occur in complex multi-turn flows as of 1.54.0. Check the SK GitHub releases changelog for entries mentioning function calling or tool call ordering.

Further Reading

⚠ Production Considerations

  • The ordering error does not occur on every request — only when the tool call → tool result sequence is interrupted. This makes it an intermittent production bug that is hard to reproduce in tests. Add ChatHistory logging at DEBUG level and include the message role sequence in your error logs to capture the state when it fails.
  • FunctionInvokingChatClient is a MEAI middleware. If you also have SK FunctionInvocationFilters registered, both may attempt to handle function calls. Use one approach only — either MEAI's FunctionInvokingChatClient OR SK's auto-invoke with filters, not both.

Enjoying this article?

Get weekly .NET + AI insights delivered to your inbox. No spam.

Subscribe Free →

🧠 Architect’s Note

For new multi-turn applications with complex function calling, prefer FunctionInvokingChatClient from MEAI over SK's auto-invoke. MEAI's implementation has cleaner message ordering semantics and benefits from more focused testing of the function calling loop.

AI-Friendly Summary

Summary

The SK 400 model_error 'tool_calls must be followed by tool messages' occurs when ChatHistory has a message between an assistant's tool call and its tool result. Workarounds: (1) post-process ChatHistory to fix ordering violations, (2) use MEAI's FunctionInvokingChatClient which handles ordering correctly, or (3) disable auto-invoke and manage the function call loop manually.

Key Takeaways

  • Tool result messages must immediately follow their paired tool call — no messages in between
  • This is SK Issue #7626 — occurs in multi-turn conversations with auto function calling
  • FunctionInvokingChatClient from MEAI handles message ordering correctly
  • Manual function calling loop is the most reliable workaround
  • Scan ChatHistory before each call to detect ordering violations proactively

Implementation Checklist

  • Upgrade to latest SK 1.54.0 — some ordering bugs are fixed in recent releases
  • Switch to FunctionInvokingChatClient via ChatClientBuilder.UseFunctionInvocation()
  • Or implement manual function call loop with FunctionChoiceBehavior.None()
  • Add ChatHistory validation before each multi-turn AI call
  • Log ChatHistory contents at DEBUG level for easier diagnosis

Frequently Asked Questions

What causes the tool_calls must be followed by tool messages error in Semantic Kernel?

This error (SK Issue #7626) occurs in multi-turn conversations when a user message gets inserted into ChatHistory between an assistant's tool call message and the corresponding tool result message. Azure OpenAI requires tool results to immediately follow their tool call — any other message in between triggers a 400 model_error.

When does the tool call ordering error happen?

It typically occurs in multi-turn conversations with FunctionChoiceBehavior.Auto() when a user sends a follow-up message while a previous function call result is still in the chat history but hasn't been properly paired. It can also occur when manually building ChatHistory and forgetting to pair tool calls with results.

How do I fix the tool_calls ordering error without disabling function calling?

Option 1: Post-process ChatHistory to ensure all AuthorRole.Tool messages immediately follow their paired AuthorRole.Assistant messages with tool calls. Option 2: Use MEAI's FunctionInvokingChatClient which handles message ordering correctly internally. Option 3: Disable auto-invoke and implement the function call loop manually.

What is FunctionInvokingChatClient from MEAI and how does it fix this?

FunctionInvokingChatClient (from Microsoft.Extensions.AI) is a middleware layer that handles the function calling loop, including correct message ordering. Register it with ChatClientBuilder.UseFunctionInvocation(). It manages tool call and tool result message pairing correctly, avoiding the ordering bug in SK's auto-invoke path.

Is SK Issue #7626 fixed in Semantic Kernel 1.54.0?

The issue has had partial fixes in recent SK releases but can still occur in specific multi-turn scenarios as of 1.54.0. The most reliable workaround remains using FunctionInvokingChatClient from MEAI or implementing the function call loop manually rather than relying on SK's auto-invoke for complex multi-turn conversations.

How do I detect if my ChatHistory has ordering violations?

Scan your ChatHistory for any message with AuthorRole other than Assistant following an Assistant message that has ToolCallContents. If a UserMessage or SystemMessage appears between an AssistantWithToolCall and a Tool message, you have an ordering violation that will cause the 400 error.

What is the correct ChatHistory message order for function calling?

The required order is: (1) User message, (2) Assistant message with tool call(s) in its content, (3) Tool result message(s) — one per tool call, in the same order as the calls, (4) Next assistant response. Any message between steps 2 and 3 causes the 400 model_error.

You Might Also Enjoy

Was this article useful?

Feedback is anonymous and helps us improve content quality.

Discussion

Engineering discussion powered by GitHub Discussions.

#Semantic Kernel #Tool Call Error #Error Fix #Function Calling #.NET AI