Skip to content

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

@AkiKurisu

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

Metadata

Metadata

Assignees

Labels

.NETbugSomething isn't workingv1.0Features being tracked for the version 1.0 GA

Type

Projects

Status

No status

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions