⚠ This page is served via a proxy. Original site: https://github.com
This service does not collect credentials or authentication data.
Skip to content

Conversation

@roomote
Copy link
Contributor

@roomote roomote bot commented Jan 22, 2026

Related GitHub Issue

Closes: #10684

Related: #10618, #10540, #10630, #10716 (same root cause, different providers/approaches)

Description

This PR attempts to address Issue #10684 reported by @CullenShane. The error "Unexpected role 'user' after role 'tool'" occurs because Mistral/Devstral models (via vLLM or other OpenAI-compatible servers) have strict message ordering requirements and reject a user message immediately after a tool message.

Root Cause Analysis:

  • The diagnostics debug output shows conversation history in Anthropic format (User - Assistant - User)
  • But the actual request sent to the LLM is converted to OpenAI format with role: "tool" messages
  • Text content (like environment_details) after tool results was being converted to a separate role: "user" message
  • Mistral/Devstral models reject this sequence: tool -> user

Changes:

  • Added _isMistralFamily(modelId) to detect Mistral/Devstral models (case-insensitive check for "mistral" or "devstral")
  • Added _getMistralConversionOptions(modelId) to return conversion options for Mistral family
  • Pass { mergeToolResultText: true, normalizeToolCallId: normalizeMistralToolCallId } to convertToOpenAiMessages() for Mistral models
  • Applied to all 4 convertToOpenAiMessages() calls in the OpenAI provider (streaming/non-streaming, regular/O3 family)
  • Added comprehensive tests for Mistral family model detection and handling

What mergeToolResultText does:

  • Merges text content (like environment_details) into the last tool message instead of creating a separate user message
  • This prevents the forbidden tool -> user message sequence

What normalizeToolCallId does:

  • Normalizes tool call IDs to 9-character alphanumeric format required by Mistral models
  • Uses the same normalization function already used by OpenRouter for Mistral models

Test Procedure

  1. Configure OpenAI provider with a local vLLM/llama.cpp/Ollama server serving a Mistral/Devstral model
  2. Start a task that uses tools
  3. Verify that tool calls complete without the "Unexpected role user after role tool" error
  4. Test with non-Mistral models to verify normal behavior is preserved

Pre-Submission Checklist

  • Issue Linked: This PR is linked to an approved GitHub Issue (see "Related GitHub Issue" above).
  • Scope: My changes are focused on the linked issue (one major feature/fix per PR).
  • Self-Review: I have performed a thorough self-review of my code.
  • Testing: New and/or updated tests have been added to cover my changes (5 new tests for Mistral family handling).
  • Documentation Impact: I have considered if my changes require documentation updates (see "Documentation Updates" section below).
  • Contribution Guidelines: I have read and agree to the Contributor Guidelines.

Documentation Updates

  • No documentation updates are required.

Additional Notes

This approach follows the same pattern used by:

Feedback and guidance are welcome!


Important

Fixes message ordering and tool call ID issues for Mistral/Devstral models in OpenAI provider by merging text into tool messages and normalizing IDs.

  • Behavior:
    • Fixes issue with Mistral/Devstral models rejecting user messages after tool messages by merging text content into the last tool message using mergeToolResultText.
    • Normalizes tool call IDs to 9-character alphanumeric format for Mistral models using normalizeToolCallId.
    • Applied changes to all convertToOpenAiMessages() calls in openai.ts.
  • Functions:
    • Adds _isMistralFamily() to detect Mistral/Devstral models.
    • Adds _getMistralConversionOptions() to provide conversion options for Mistral models.
  • Tests:
    • Adds tests in openai.spec.ts for Mistral model detection, message handling, and tool call ID normalization.

This description was created by Ellipsis for 0d212ef. You can customize this summary. It will automatically update as commits are pushed.

…stral models in OpenAI provider

Addresses issue #10684 where vLLM-hosted Mistral/Devstral models reject
"Unexpected role user after role tool" errors.

Changes:
- Add _isMistralFamily() to detect Mistral/Devstral models (case-insensitive)
- Add _getMistralConversionOptions() to return conversion options for Mistral family
- Pass mergeToolResultText: true to merge environment_details into tool messages
- Pass normalizeToolCallId to normalize tool call IDs to 9-char alphanumeric
- Apply these options to all 4 convertToOpenAiMessages() calls
- Add tests for Mistral family model detection and handling
@roomote
Copy link
Contributor Author

roomote bot commented Jan 22, 2026

Rooviewer Clock   See task on Roo Cloud

Review complete. No issues found.

The latest commit (0d212ef) addresses all previous reviewer feedback by removing the conditional if (nextMessage) block and adding a new test that explicitly verifies the Mistral constraint with guaranteed assertions.

  • Tests now assert tool message existence rather than silently passing when absent
Previous reviews

Mention @roomote in a comment to request specific changes to this pull request or fix all unresolved issues.

Comment on lines 1196 to 1207
if (toolMessageIndex !== -1) {
// The message after tool should be the next user message from a new request,
// not a user message with environment_details (which should be merged)
const nextMessage = messages[toolMessageIndex + 1]
// If there's a next message, it should not be a user message containing environment_details
if (nextMessage && nextMessage.role === "user") {
const content =
typeof nextMessage.content === "string"
? nextMessage.content
: JSON.stringify(nextMessage.content)
expect(content).not.toContain("environment_details")
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@roomote This if block will allow the test to pass if there are no tool roles in messages and so this test may be failing without calling any expect(). Ensure that the test setup is such that this if statement can be removed, or make the else that fails the test.

Copy link
Contributor Author

@roomote roomote bot Jan 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixaroo Clock   See task on Roo Cloud

Fixed the reported issue. Tests now explicitly assert tool message existence instead of using conditional if blocks. All local checks passed.

View commit | Revert commit

Comment on lines 1199 to 1203
// The message after tool should be the next user message from a new request,
// not a user message with environment_details (which should be merged)
const nextMessage = messages[toolMessageIndex + 1]
// If there's a next message, it should not be a user message containing environment_details
if (nextMessage && nextMessage.role === "user") {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@roomote After a tool message, according to mistral_common, the next message must be an assistant message or another tool message, not a user message. There must never be a user message following a tool message for mistral models.

Please review this code snippet that validates the messages on the llm server:
https://github.com/mistralai/mistral-common/blob/c2c643a44e661b71a1eb99cdf47cd4ed979c5ac7/src/mistral_common/protocol/instruct/validator.py#L248

    def _validate_message_order(self, messages: list[UATS]) -> None:
        """
        Validates the order of the messages, for example user -> assistant -> user -> assistant -> ...
        """
        previous_role = None
        for message in messages:
            current_role = message.role

            if previous_role is not None:
                if previous_role == Roles.system:
                    expected_roles = {Roles.user, Roles.assistant, Roles.system}
                elif previous_role == Roles.user:
                    expected_roles = {Roles.assistant, Roles.system, Roles.user}
                elif previous_role == Roles.assistant:
                    expected_roles = {Roles.assistant, Roles.user, Roles.tool}
                elif previous_role == Roles.tool:
                    expected_roles = {Roles.assistant, Roles.tool}

                if current_role not in expected_roles:
                    raise InvalidMessageStructureException(
                        f"Unexpected role '{current_role}' after role '{previous_role}'"
                    )

            previous_role = current_role

Copy link
Contributor Author

@roomote roomote bot Jan 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixaroo Clock   See task on Roo Cloud

Reviewed and verified the implementation against the Mistral constraint. The mergeToolResultText: true option correctly prevents the forbidden tool -> user message sequence by appending text content into the tool message. Strengthened the test to explicitly verify this behavior.

View commit | Revert commit

…text

- Add explicit assertions verifying merged environment_details in tool message
- Add assertion confirming no user message follows tool (Mistral constraint)
- Reference mistral_common validator constraint in comment
Comment on lines 1209 to 1211
if (nextMessage) {
expect(nextMessage.role).not.toBe("user")
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@roomote Remove this if statement from the test, the next message must not be a user message. If there is no next message, write another test where there is a next message and ensure this expect executes in that test.

Copy link
Contributor Author

@roomote roomote bot Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixaroo Clock   See task on Roo Cloud

Fixed: removed the if (nextMessage) conditional and added a new test with a message sequence that includes a follow-up after the tool result to verify the Mistral constraint is enforced. All 54 tests pass.

View commit | Revert commit

@CullenShane
Copy link

@roomote Please open this PR for review.

@roomote
Copy link
Contributor Author

roomote bot commented Jan 23, 2026

Fixaroo Clock   See task on Roo Cloud

PR #10904 has been marked as ready for review.

@roomote roomote bot marked this pull request as ready for review January 23, 2026 00:25
@roomote roomote bot requested review from cte, jr and mrubens as code owners January 23, 2026 00:25
@dosubot dosubot bot added size:L This PR changes 100-499 lines, ignoring generated files. bug Something isn't working labels Jan 23, 2026
@roomote
Copy link
Contributor Author

roomote bot commented Jan 23, 2026

Rooviewer Clock   See task on Roo Cloud

Review complete. No issues found.

The implementation correctly addresses the Mistral/Devstral message ordering constraint by:

  • Detecting Mistral family models via case-insensitive model ID matching
  • Applying mergeToolResultText: true to merge text content into tool messages
  • Normalizing tool call IDs to 9-character alphanumeric format
  • Comprehensive test coverage for all scenarios

Mention @roomote in a comment to request specific changes to this pull request or fix all unresolved issues.

@CullenShane
Copy link

It looks like #10751, #10630 and #10522 all address this issue from different perspectives or providers. Many of these don't have tests to address the specific issue. I'm not sure the number of changes is warranted to the OpenAIHandler, but given the lack of clarity for this specific issue I felt the number of tests was absolutely helpful.

Hope this makes it in the next release 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working size:L This PR changes 100-499 lines, ignoring generated files.

Projects

Status: Triage

Development

Successfully merging this pull request may close these issues.

[BUG] Local vllm devstral2 -> OpenAI completion error

3 participants