From db061661cc4e034c515790bb1509814d644d79a3 Mon Sep 17 00:00:00 2001 From: Roo Code Date: Sat, 10 Jan 2026 05:40:55 +0000 Subject: [PATCH] feat: add strict tool message ordering setting for OpenAI Compatible providers Adds a new setting `openAiStrictToolMessageOrdering` to OpenAI Compatible providers that enables merging text content after tool results into the last tool message. This fixes "Unexpected role user after role tool" errors with NVIDIA NIM, OpenRouter, and other strict OpenAI-compatible APIs. When enabled, environment_details and other text content that follows tool_result messages are merged into the last tool message instead of being sent as a separate user message. Fixes #10540 --- packages/types/src/provider-settings.ts | 1 + src/api/providers/openai.ts | 24 +++++++++++++++---- .../settings/providers/OpenAICompatible.tsx | 10 ++++++++ webview-ui/src/i18n/locales/en/settings.json | 4 +++- 4 files changed, 34 insertions(+), 5 deletions(-) diff --git a/packages/types/src/provider-settings.ts b/packages/types/src/provider-settings.ts index 3b3717ad45..8cad064732 100644 --- a/packages/types/src/provider-settings.ts +++ b/packages/types/src/provider-settings.ts @@ -250,6 +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. }) const ollamaSchema = baseProviderSettingsSchema.extend({ diff --git a/src/api/providers/openai.ts b/src/api/providers/openai.ts index 43a4fe7ae3..0906e4fd8a 100644 --- a/src/api/providers/openai.ts +++ b/src/api/providers/openai.ts @@ -122,7 +122,11 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl } } - convertedMessages = [systemMessage, ...convertToOpenAiMessages(messages)] + // 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 })] if (modelInfo.supportsPromptCache) { // Note: the following logic is copied from openrouter: @@ -224,11 +228,15 @@ 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)], + : [systemMessage, ...convertToOpenAiMessages(messages, { mergeToolResultText })], ...(metadata?.tools && { tools: this.convertToolsForOpenAI(metadata.tools) }), ...(metadata?.tool_choice && { tool_choice: metadata.tool_choice }), ...(metadata?.toolProtocol === "native" && { @@ -342,6 +350,10 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl 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: [ @@ -349,7 +361,7 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl role: "developer", content: `Formatting re-enabled\n${systemPrompt}`, }, - ...convertToOpenAiMessages(messages), + ...convertToOpenAiMessages(messages, { mergeToolResultText }), ], stream: true, ...(isGrokXAI ? {} : { stream_options: { include_usage: true } }), @@ -379,6 +391,10 @@ 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: [ @@ -386,7 +402,7 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl role: "developer", content: `Formatting re-enabled\n${systemPrompt}`, }, - ...convertToOpenAiMessages(messages), + ...convertToOpenAiMessages(messages, { mergeToolResultText }), ], reasoning_effort: modelInfo.reasoningEffort as "low" | "medium" | "high" | undefined, temperature: undefined, diff --git a/webview-ui/src/components/settings/providers/OpenAICompatible.tsx b/webview-ui/src/components/settings/providers/OpenAICompatible.tsx index 4eea6f09f1..25fefd2639 100644 --- a/webview-ui/src/components/settings/providers/OpenAICompatible.tsx +++ b/webview-ui/src/components/settings/providers/OpenAICompatible.tsx @@ -158,6 +158,16 @@ export const OpenAICompatible = ({ onChange={handleInputChange("openAiStreamingEnabled", noTransform)}> {t("settings:modelInfo.enableStreaming")} +
+ + {t("settings:providers.openAiStrictToolMessageOrdering")} + +
+ {t("settings:providers.openAiStrictToolMessageOrderingDescription")} +
+