Skip to content

.NET: [Bug]: ChatCompletionRequestMessage.ToChatMessage() hardcodes ChatRole.User, breaking multi-turn conversations #4289

Closed

Description

@AkiKurisu

Description

What happened?

When using the ChatCompletions hosting endpoint (MapOpenAIChatCompletions), all incoming messages in a multi-turn conversation are converted to ChatRole.User, regardless of their actual role. This means assistant messages, system messages, developer messages, and tool messages are all treated as user messages when forwarded to the underlying AI agent.

What did you expect to happen?

Each message's original role (user, assistant, system, developer, tool) should be preserved during the conversion from ChatCompletionRequestMessage to ChatMessage, so the LLM can correctly distinguish between its own previous responses and user inputs.

Steps to reproduce the issue

  1. Host an agent using MapOpenAIChatCompletions.
  2. Send a multi-turn ChatCompletions request with messages containing different roles (e.g., a system message, a user message, and an assistant message).
  3. Observe that all messages are passed to the agent with ChatRole.User, causing the LLM to lose track of conversation history.

Root Cause

In ChatCompletionRequestMessage.cs, the base ToChatMessage() method hardcodes ChatRole.User:

public virtual ChatMessage ToChatMessage()
{
    if (this.Content.IsText)
    {
        return new(ChatRole.User, this.Content.Text);  // ← Always User
    }
    else if (this.Content.IsContents)
    {
        var aiContents = this.Content.Contents.Select(MessageContentPartConverter.ToAIContent).Where(c => c is not null).ToList();
        return new ChatMessage(ChatRole.User, aiContents!);  // ← Always User
    }

    throw new InvalidOperationException("MessageContent has no value");
}

None of the derived types (DeveloperMessage, SystemMessage, UserMessage, AssistantMessage, ToolMessage) override this method, so they all inherit the hardcoded ChatRole.User. FunctionMessage is the only one that overrides, but it also hardcodes ChatRole.User.

For comparison, the Responses API path (InputMessage.ToChatMessage()) correctly uses this.Role:

public ChatMessage ToChatMessage()
{
    if (this.Content.IsText)
    {
        return new ChatMessage(this.Role, this.Content.Text);  // ← Correct
    }
    // ...
}

Suggested Fix

Replace the hardcoded ChatRole.User in the base ToChatMessage() with a role derived from the subclass's Role property:

public virtual ChatMessage ToChatMessage()
{
    var role = new ChatRole(this.Role);
    if (this.Content.IsText)
    {
        return new(role, this.Content.Text);
    }
    else if (this.Content.IsContents)
    {
        var aiContents = this.Content.Contents.Select(MessageContentPartConverter.ToAIContent).Where(c => c is not null).ToList();
        return new ChatMessage(role, aiContents!);
    }

    throw new InvalidOperationException("MessageContent has no value");
}

Also fix FunctionMessage.ToChatMessage() to use new ChatRole(this.Role) instead of ChatRole.User.

Code Sample

// Client sends a standard multi-turn ChatCompletions request:
// POST /v1/chat/completions
// {
//   "model": "gpt-4o",
//   "messages": [
//     { "role": "system", "content": "You are a helpful assistant." },
//     { "role": "user", "content": "Hello!" },
//     { "role": "assistant", "content": "Hi there! How can I help?" },
//     { "role": "user", "content": "What did I just say?" }
//   ]
// }
//
// Expected: agent receives messages with correct roles (system, user, assistant, user)
// Actual: agent receives ALL messages with role "user", making the LLM unable to
//         identify which messages were its own responses

Error Messages / Stack Traces

No error/exception is thrown. The bug manifests as incorrect LLM behavior — the model cannot identify its own previous responses in the conversation history because all messages appear as `user` role.

Package Versions

Microsoft.Agents.AI.Hosting.OpenAI: 1.0.0-rc2

.NET Version

.Net 10.0

Additional Context

No response

Activity

  1. added
    bugUsage: [Issues], Target: all issues (Legacy, prefer issue type: bug)
    .NETUsage: [Issues, PRs], Target: .Net
    on Feb 26, 2026
  2. AkiKurisu commented on Feb 26, 2026

    @AkiKurisu
    ContributorAuthor

    Additional Context

    • This bug only affects the ChatCompletions hosting path (MapOpenAIChatCompletions). The Responses hosting path (MapOpenAIResponses) correctly preserves roles via InputMessage.ToChatMessage().
    • The JSON deserialization layer works correctly — [JsonPolymorphic] with role discriminator properly creates the right subtypes (AssistantMessage, SystemMessage, etc.) with the correct Role property. The problem is solely in the ToChatMessage() conversion that ignores this property.
    • This issue was discovered in DotBot, an open-source AI agent project that uses MapOpenAIChatCompletions to expose an OpenAI-compatible API endpoint (ApiChannelService.cs#L205). When clients like Chatbox send multi-turn conversations through this endpoint, the LLM fails to maintain coherent dialogue because all historical messages — including its own previous assistant replies — are converted to user role.
  3. added and removed
    triageUsage: [Issues], Target: All issues that still need to be triaged
    on Feb 26, 2026
  4. moved this from Planned to In Progress in Agent Frameworkon Mar 23, 2026
  5. moved this from In Progress to Done in Agent Frameworkon Mar 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

.NETUsage: [Issues, PRs], Target: .NetbugUsage: [Issues], Target: all issues (Legacy, prefer issue type: bug)

Type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions