mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-05 08:10:14 +00:00
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:
parent
db061661cc
commit
4111aa6bcf
4 changed files with 23 additions and 23 deletions
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue