diff --git a/packages/types/src/provider-settings.ts b/packages/types/src/provider-settings.ts
index 8cad064732..cc74f9f43d 100644
--- a/packages/types/src/provider-settings.ts
+++ b/packages/types/src/provider-settings.ts
@@ -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({
diff --git a/src/api/providers/openai.ts b/src/api/providers/openai.ts
index 0906e4fd8a..01792b99fb 100644
--- a/src/api/providers/openai.ts
+++ b/src/api/providers/openai.ts
@@ -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,
diff --git a/webview-ui/src/components/settings/providers/OpenAICompatible.tsx b/webview-ui/src/components/settings/providers/OpenAICompatible.tsx
index 25fefd2639..cc1777ca76 100644
--- a/webview-ui/src/components/settings/providers/OpenAICompatible.tsx
+++ b/webview-ui/src/components/settings/providers/OpenAICompatible.tsx
@@ -162,10 +162,10 @@ export const OpenAICompatible = ({