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 = ({ - {t("settings:providers.openAiStrictToolMessageOrdering")} + {t("settings:providers.openAiStrictToolMessageOrdering.label")}
- {t("settings:providers.openAiStrictToolMessageOrderingDescription")} + {t("settings:providers.openAiStrictToolMessageOrdering.description")}
diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index 0b78ee2196..fd87ca2fd1 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -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."