Description
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
- Host an agent using
MapOpenAIChatCompletions. - Send a multi-turn ChatCompletions request with messages containing different roles (e.g., a
systemmessage, ausermessage, and anassistantmessage). - 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 responsesError 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
Type
Projects
- StatusShow more project fieldsDone
Activity
AkiKurisu commentedon Feb 26, 2026
Additional Context
MapOpenAIChatCompletions). The Responses hosting path (MapOpenAIResponses) correctly preserves roles viaInputMessage.ToChatMessage().[JsonPolymorphic]with role discriminator properly creates the right subtypes (AssistantMessage,SystemMessage, etc.) with the correctRoleproperty. The problem is solely in theToChatMessage()conversion that ignores this property.MapOpenAIChatCompletionsto 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 touserrole.