feat: add strict tool message ordering setting for OpenAI-compatible providers

Adds a new setting `openAiStrictToolMessageOrdering` for OpenAI-compatible providers
that, when enabled, merges text content after tool results into the last tool message
instead of creating a separate user message.

This fixes the "Unexpected role user after role tool" error that occurs with
providers like NVIDIA NIM and Devstral that enforce strict message ordering rules.

Fixes #10540
This commit is contained in:
Roo Code 2026-01-10 05:44:58 +00:00
parent db061661cc
commit 4111aa6bcf
4 changed files with 23 additions and 23 deletions

View file

@ -250,7 +250,7 @@ const openAiSchema = baseProviderSettingsSchema.extend({
openAiStreamingEnabled: z.boolean().optional(),
openAiHostHeader: z.string().optional(), // Keep temporarily for backward compatibility during migration.
openAiHeaders: z.record(z.string(), z.string()).optional(),
openAiStrictToolMessageOrdering: z.boolean().optional(), // Merge text after tool results to avoid "user after tool" errors.
openAiStrictToolMessageOrdering: z.boolean().optional(), // For providers that don't allow user messages after tool messages.
})
const ollamaSchema = baseProviderSettingsSchema.extend({

View file

@ -102,6 +102,11 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
content: systemPrompt,
}
// When strict tool message ordering is enabled, merge text content after tool_results
// into the last tool message instead of creating a separate user message.
// This is required for providers like NVIDIA NIM that don't allow user messages after tool messages.
const strictToolMessageOrdering = this.options.openAiStrictToolMessageOrdering ?? false
if (this.options.openAiStreamingEnabled ?? true) {
let convertedMessages
@ -122,11 +127,10 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
}
}
// When openAiStrictToolMessageOrdering is enabled, merge text content after tool results
// into the last tool message. This fixes "user after tool" errors with NVIDIA NIM,
// OpenRouter, and other strict OpenAI-compatible APIs.
const mergeToolResultText = this.options.openAiStrictToolMessageOrdering ?? false
convertedMessages = [systemMessage, ...convertToOpenAiMessages(messages, { mergeToolResultText })]
convertedMessages = [
systemMessage,
...convertToOpenAiMessages(messages, { mergeToolResultText: strictToolMessageOrdering }),
]
if (modelInfo.supportsPromptCache) {
// Note: the following logic is copied from openrouter:
@ -228,15 +232,14 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
yield this.processUsageMetrics(lastUsage, modelInfo)
}
} else {
// When openAiStrictToolMessageOrdering is enabled, merge text content after tool results
// into the last tool message. This fixes "user after tool" errors with NVIDIA NIM,
// OpenRouter, and other strict OpenAI-compatible APIs.
const mergeToolResultText = this.options.openAiStrictToolMessageOrdering ?? false
const requestOptions: OpenAI.Chat.Completions.ChatCompletionCreateParamsNonStreaming = {
model: modelId,
messages: deepseekReasoner
? convertToR1Format([{ role: "user", content: systemPrompt }, ...messages])
: [systemMessage, ...convertToOpenAiMessages(messages, { mergeToolResultText })],
: [
systemMessage,
...convertToOpenAiMessages(messages, { mergeToolResultText: strictToolMessageOrdering }),
],
...(metadata?.tools && { tools: this.convertToolsForOpenAI(metadata.tools) }),
...(metadata?.tool_choice && { tool_choice: metadata.tool_choice }),
...(metadata?.toolProtocol === "native" && {
@ -346,14 +349,11 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
): ApiStream {
const modelInfo = this.getModel().info
const methodIsAzureAiInference = this._isAzureAiInference(this.options.openAiBaseUrl)
const strictToolMessageOrdering = this.options.openAiStrictToolMessageOrdering ?? false
if (this.options.openAiStreamingEnabled ?? true) {
const isGrokXAI = this._isGrokXAI(this.options.openAiBaseUrl)
// When openAiStrictToolMessageOrdering is enabled, merge text content after tool results
// into the last tool message. This fixes "user after tool" errors with NVIDIA NIM,
// OpenRouter, and other strict OpenAI-compatible APIs.
const mergeToolResultText = this.options.openAiStrictToolMessageOrdering ?? false
const requestOptions: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming = {
model: modelId,
messages: [
@ -361,7 +361,7 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
role: "developer",
content: `Formatting re-enabled\n${systemPrompt}`,
},
...convertToOpenAiMessages(messages, { mergeToolResultText }),
...convertToOpenAiMessages(messages, { mergeToolResultText: strictToolMessageOrdering }),
],
stream: true,
...(isGrokXAI ? {} : { stream_options: { include_usage: true } }),
@ -391,10 +391,6 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
yield* this.handleStreamResponse(stream)
} else {
// When openAiStrictToolMessageOrdering is enabled, merge text content after tool results
// into the last tool message. This fixes "user after tool" errors with NVIDIA NIM,
// OpenRouter, and other strict OpenAI-compatible APIs.
const mergeToolResultText = this.options.openAiStrictToolMessageOrdering ?? false
const requestOptions: OpenAI.Chat.Completions.ChatCompletionCreateParamsNonStreaming = {
model: modelId,
messages: [
@ -402,7 +398,7 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
role: "developer",
content: `Formatting re-enabled\n${systemPrompt}`,
},
...convertToOpenAiMessages(messages, { mergeToolResultText }),
...convertToOpenAiMessages(messages, { mergeToolResultText: strictToolMessageOrdering }),
],
reasoning_effort: modelInfo.reasoningEffort as "low" | "medium" | "high" | undefined,
temperature: undefined,

View file

@ -162,10 +162,10 @@ export const OpenAICompatible = ({
<Checkbox
checked={apiConfiguration?.openAiStrictToolMessageOrdering ?? false}
onChange={handleInputChange("openAiStrictToolMessageOrdering", noTransform)}>
{t("settings:providers.openAiStrictToolMessageOrdering")}
{t("settings:providers.openAiStrictToolMessageOrdering.label")}
</Checkbox>
<div className="text-sm text-vscode-descriptionForeground ml-6">
{t("settings:providers.openAiStrictToolMessageOrderingDescription")}
{t("settings:providers.openAiStrictToolMessageOrdering.description")}
</div>
</div>
<div>

View file

@ -498,6 +498,10 @@
},
"resetDefaults": "Reset to Defaults"
},
"openAiStrictToolMessageOrdering": {
"label": "Strict tool message ordering",
"description": "Enable for providers like NVIDIA NIM and Devstral that require strict message ordering. When enabled, text content after tool results is merged into the last tool message instead of creating a separate user message."
},
"rateLimitSeconds": {
"label": "Rate limit",
"description": "Minimum time between API requests."