-
Notifications
You must be signed in to change notification settings - Fork 1.2k
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
Status