dotnet/machinelearning · error · InvalidOperationException

Invalid role.

Error message

Invalid role.

What it means

Mistral_7B_0_3ChatTemplateBuilder.BuildPrompt formats each message in a ChatHistory into Mistral's [INST]...[/INST] template. Only ToolCall/ToolResult/aggregate messages and TextMessage with Role.User or Role.Assistant are supported; any other role (e.g. System passed as a TextMessage after the first message, or a custom role) falls into the default arm and throws InvalidOperationException.

Source

Thrown at src/Microsoft.ML.GenAI.Mistral/Mistral_7B_0_3ChatTemplateBuilder.cs:60

        var sb = new StringBuilder();
        foreach (var message in firstSequence)
        {
            // skip system
            if (message.GetRole() == Role.System)
            {
                continue;
            }

            var content = message.GetContent()!;
            sb.Append(message switch
            {
                ToolCallMessage toolCallMessage => BuildFromToolCallMessage(toolCallMessage),
                ToolCallResultMessage toolCallResultMessage => BuildFromToolCallResultMessage(toolCallResultMessage),
                ToolCallAggregateMessage toolCallAggregateMessage => BuildFromAggregrateToolCallMessage(toolCallAggregateMessage),
                TextMessage when message.GetRole() == Role.User => $"[INST]{content.Trim()}[/INST]",
                TextMessage when message.GetRole() == Role.Assistant => $"{content.Trim()}</s>",
                _ => throw new InvalidOperationException("Invalid role.")
            });
        }

        // insert [AVAILABLE TOOLS] section if tools are provided
        if (tools?.Any() == true)
        {
            var schemas = tools.Select(t => new
            {
                type = "function",
                function = new
                {
                    name = t.Name,
                    description = t.Description,
                    parameters = BuildJsonSchemaFromFunctionContract(t)
                }
            });
            var schemaPrompt = JsonSerializer.Serialize(schemas);

View on GitHub (pinned to 7b76e69cf9)

Solutions

  1. Ensure all messages are TextMessage with Role.User or Role.Assistant (or tool-call message types)
  2. Fold system instructions into the first user message instead of a separate System TextMessage
  3. Add a pattern arm for the extra role you need, e.g. TextMessage when message.GetRole() == Role.System => system-style formatting
  4. Catch InvalidOperationException around BuildPrompt and log message.GetRole() to identify the offending message

Example fix

// before
new TextMessage(Role.System, "You are helpful.")
// after
new TextMessage(Role.User, "You are helpful.\n\n" + userContent)
Defensive patterns

Strategy: validation

Validate before calling

bool allRolesValid = messages.OfType<TextMessage>().All(m => m.GetRole() == Role.User || m.GetRole() == Role.Assistant);
if (!allRolesValid) throw new ArgumentException("Mistral template supports only User/Assistant text messages");

Type guard

static bool IsSupportedMessage(Message m) =>
    m is ToolCallMessage or ToolCallResultMessage or ToolCallAggregateMessage ||
    (m is TextMessage t && (t.GetRole() == Role.User || t.GetRole() == Role.Assistant));

Try / catch

try { prompt = builder.BuildPrompt(messages, systemMessage, tools); }
catch (InvalidOperationException ex) { log.Error("Unsupported role in chat history", ex); throw; }

Prevention

When it happens

Trigger: Calling BuildPrompt(IEnumerable<Message> chatHistory, ...) with a TextMessage whose GetRole() is neither User nor Assistant — most commonly a System TextMessage appearing after position 0, or a message whose role string was parsed as something unexpected.

Common situations: Porting code from another chat template (e.g. Llama) that accepts a System role anywhere in the history; hand-built ChatHistory with a system-style TextMessage; a new Role enum value added upstream but not handled by the Mistral template.

Understand the failure class

Background: Invalid enum value errors: "Unknown type", "Invalid scope", "must be one of" — when a string is not on the library's allowed list — this error's family across 23 libraries.

Related errors


AI-assisted analysis of dotnet/machinelearning@7b76e69cf9 (2026-09-11). Data as JSON: /api/errors/092a4ad5baa88fc1. Report an issue: GitHub.