-
Notifications
You must be signed in to change notification settings - Fork 2.8k
fix: apply mergeToolResultText and normalize tool IDs for Mistral/Devstral models in OpenAI provider #10904
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
…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
Review complete. No issues found. The latest commit (0d212ef) addresses all previous reviewer feedback by removing the conditional
Mention @roomote in a comment to request specific changes to this pull request or fix all unresolved issues. |
| 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") | ||
| } |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixaroo
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.
| // 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") { |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixaroo
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.
…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
| if (nextMessage) { | ||
| expect(nextMessage.role).not.toBe("user") | ||
| } |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixaroo
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.
…ter-tool constraint
|
@roomote Please open this PR for review. |
PR #10904 has been marked as ready for review. |
Review complete. No issues found. The implementation correctly addresses the Mistral/Devstral message ordering constraint by:
Mention @roomote in a comment to request specific changes to this pull request or fix all unresolved issues. |
|
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 👍 |
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:
role: "tool"messagesenvironment_details) after tool results was being converted to a separaterole: "user"messageChanges:
_isMistralFamily(modelId)to detect Mistral/Devstral models (case-insensitive check for "mistral" or "devstral")_getMistralConversionOptions(modelId)to return conversion options for Mistral family{ mergeToolResultText: true, normalizeToolCallId: normalizeMistralToolCallId }toconvertToOpenAiMessages()for Mistral modelsconvertToOpenAiMessages()calls in the OpenAI provider (streaming/non-streaming, regular/O3 family)What
mergeToolResultTextdoes:What
normalizeToolCallIddoes:Test Procedure
Pre-Submission Checklist
Documentation Updates
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.
mergeToolResultText.normalizeToolCallId.convertToOpenAiMessages()calls inopenai.ts._isMistralFamily()to detect Mistral/Devstral models._getMistralConversionOptions()to provide conversion options for Mistral models.openai.spec.tsfor Mistral model detection, message handling, and tool call ID normalization.This description was created by
for 0d212ef. You can customize this summary. It will automatically update as commits are pushed.