diff --git a/src/api/providers/openai.ts b/src/api/providers/openai.ts index 9d632fbdf4c..2949099bfe2 100644 --- a/src/api/providers/openai.ts +++ b/src/api/providers/openai.ts @@ -122,7 +122,10 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl } } - convertedMessages = [systemMessage, ...convertToOpenAiMessages(messages)] + // Use mergeToolResultText to merge text content into the last tool message + // This prevents "Unexpected role 'user' after role 'tool'" errors with + // strict OpenAI-compatible servers (vLLM, llama.cpp, Ollama with Mistral, etc.) + convertedMessages = [systemMessage, ...convertToOpenAiMessages(messages, { mergeToolResultText: true })] if (modelInfo.supportsPromptCache) { // Note: the following logic is copied from openrouter: @@ -225,11 +228,13 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl yield this.processUsageMetrics(lastUsage, modelInfo) } } else { + // Use mergeToolResultText for non-streaming mode as well to prevent + // "Unexpected role 'user' after role 'tool'" errors with strict servers const requestOptions: OpenAI.Chat.Completions.ChatCompletionCreateParamsNonStreaming = { model: modelId, messages: deepseekReasoner ? convertToR1Format([{ role: "user", content: systemPrompt }, ...messages]) - : [systemMessage, ...convertToOpenAiMessages(messages)], + : [systemMessage, ...convertToOpenAiMessages(messages, { mergeToolResultText: true })], ...(metadata?.tools && { tools: this.convertToolsForOpenAI(metadata.tools) }), ...(metadata?.tool_choice && { tool_choice: metadata.tool_choice }), ...(metadata?.toolProtocol === "native" && @@ -344,6 +349,7 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl if (this.options.openAiStreamingEnabled ?? true) { const isGrokXAI = this._isGrokXAI(this.options.openAiBaseUrl) + // Use mergeToolResultText for O3 family models as well const requestOptions: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming = { model: modelId, messages: [ @@ -351,7 +357,7 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl role: "developer", content: `Formatting re-enabled\n${systemPrompt}`, }, - ...convertToOpenAiMessages(messages), + ...convertToOpenAiMessages(messages, { mergeToolResultText: true }), ], stream: true, ...(isGrokXAI ? {} : { stream_options: { include_usage: true } }), @@ -382,6 +388,7 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl yield* this.handleStreamResponse(stream) } else { + // Use mergeToolResultText for O3 family non-streaming mode as well const requestOptions: OpenAI.Chat.Completions.ChatCompletionCreateParamsNonStreaming = { model: modelId, messages: [ @@ -389,7 +396,7 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl role: "developer", content: `Formatting re-enabled\n${systemPrompt}`, }, - ...convertToOpenAiMessages(messages), + ...convertToOpenAiMessages(messages, { mergeToolResultText: true }), ], reasoning_effort: modelInfo.reasoningEffort as "low" | "medium" | "high" | undefined, temperature: undefined,