From 3ded6d047e123821c6fd7430910ebad6d4cce2f1 Mon Sep 17 00:00:00 2001 From: Lars Grammel Date: Tue, 21 Apr 2026 11:11:19 +0200 Subject: [PATCH] fix: more precise default message for tool execution denial (#14656) The default tool call denial message `Tool execution denied.` led to Open GPT 5 models rejecting further calls to the same tool with different inputs. Change the default message `Tool execution denied.` to the more precise `Tool call execution denied.` Discovered during #14643 (cherry picked from commit 58a2ad7dd43ff2ba014f1acb46dfd937946104ce) --- .changeset/nine-starfishes-relax.md | 17 ++ .../src/ui/convert-to-model-messages.test.ts | 2 +- .../ai/src/ui/convert-to-model-messages.ts | 2 +- .../src/convert-to-alibaba-chat-messages.ts | 2 +- .../src/convert-to-bedrock-chat-messages.ts | 2 +- .../convert-to-anthropic-messages-prompt.ts | 3 +- .../src/convert-to-cohere-chat-prompt.ts | 2 +- .../chat/convert-to-deepseek-chat-messages.ts | 2 +- ...onvert-to-google-generative-ai-messages.ts | 2 +- .../groq/src/convert-to-groq-chat-messages.ts | 2 +- .../src/convert-to-mistral-chat-messages.ts | 2 +- .../convert-to-open-responses-input.ts | 2 +- ...vert-to-openai-compatible-chat-messages.ts | 2 +- .../chat/convert-to-openai-chat-messages.ts | 2 +- .../convert-to-openai-responses-input.test.ts | 188 ++++++++++++++++++ .../convert-to-openai-responses-input.ts | 4 +- .../xai/src/convert-to-xai-chat-messages.ts | 2 +- 17 files changed, 222 insertions(+), 16 deletions(-) create mode 100644 .changeset/nine-starfishes-relax.md diff --git a/.changeset/nine-starfishes-relax.md b/.changeset/nine-starfishes-relax.md new file mode 100644 index 000000000000..c26b3dfd1f49 --- /dev/null +++ b/.changeset/nine-starfishes-relax.md @@ -0,0 +1,17 @@ +--- +"@ai-sdk/openai-compatible": patch +"@ai-sdk/amazon-bedrock": patch +"@ai-sdk/open-responses": patch +"@ai-sdk/anthropic": patch +"@ai-sdk/deepseek": patch +"@ai-sdk/alibaba": patch +"@ai-sdk/mistral": patch +"@ai-sdk/cohere": patch +"@ai-sdk/google": patch +"@ai-sdk/openai": patch +"@ai-sdk/groq": patch +"@ai-sdk/xai": patch +"ai": patch +--- + +fix: more precise default message for tool execution denial diff --git a/packages/ai/src/ui/convert-to-model-messages.test.ts b/packages/ai/src/ui/convert-to-model-messages.test.ts index fdc6b98b3776..af5af187205d 100644 --- a/packages/ai/src/ui/convert-to-model-messages.test.ts +++ b/packages/ai/src/ui/convert-to-model-messages.test.ts @@ -1940,7 +1940,7 @@ describe('convertToModelMessages', () => { { "output": { "type": "error-text", - "value": "Tool execution denied.", + "value": "Tool call execution denied.", }, "toolCallId": "call-1", "toolName": "weather", diff --git a/packages/ai/src/ui/convert-to-model-messages.ts b/packages/ai/src/ui/convert-to-model-messages.ts index 85f3834c7632..82edb75ca40c 100644 --- a/packages/ai/src/ui/convert-to-model-messages.ts +++ b/packages/ai/src/ui/convert-to-model-messages.ts @@ -297,7 +297,7 @@ export async function convertToModelMessages( type: 'error-text' as const, value: toolPart.approval?.reason ?? - 'Tool execution denied.', + 'Tool call execution denied.', }, ...(toolPart.callProviderMetadata != null ? { providerOptions: toolPart.callProviderMetadata } diff --git a/packages/alibaba/src/convert-to-alibaba-chat-messages.ts b/packages/alibaba/src/convert-to-alibaba-chat-messages.ts index 7bbf074013a0..ba956c581f13 100644 --- a/packages/alibaba/src/convert-to-alibaba-chat-messages.ts +++ b/packages/alibaba/src/convert-to-alibaba-chat-messages.ts @@ -167,7 +167,7 @@ export function convertToAlibabaChatMessages({ contentValue = output.value; break; case 'execution-denied': - contentValue = output.reason ?? 'Tool execution denied.'; + contentValue = output.reason ?? 'Tool call execution denied.'; break; case 'content': case 'json': diff --git a/packages/amazon-bedrock/src/convert-to-bedrock-chat-messages.ts b/packages/amazon-bedrock/src/convert-to-bedrock-chat-messages.ts index defce9aeed03..8b49307c1ad8 100644 --- a/packages/amazon-bedrock/src/convert-to-bedrock-chat-messages.ts +++ b/packages/amazon-bedrock/src/convert-to-bedrock-chat-messages.ts @@ -253,7 +253,7 @@ export async function convertToBedrockChatMessages( break; case 'execution-denied': toolResultContent = [ - { text: output.reason ?? 'Tool execution denied.' }, + { text: output.reason ?? 'Tool call execution denied.' }, ]; break; case 'json': diff --git a/packages/anthropic/src/convert-to-anthropic-messages-prompt.ts b/packages/anthropic/src/convert-to-anthropic-messages-prompt.ts index 77d233d15d74..2b35f0a0afe7 100644 --- a/packages/anthropic/src/convert-to-anthropic-messages-prompt.ts +++ b/packages/anthropic/src/convert-to-anthropic-messages-prompt.ts @@ -437,7 +437,8 @@ export async function convertToAnthropicMessagesPrompt({ contentValue = output.value; break; case 'execution-denied': - contentValue = output.reason ?? 'Tool execution denied.'; + contentValue = + output.reason ?? 'Tool call execution denied.'; break; case 'json': case 'error-json': diff --git a/packages/cohere/src/convert-to-cohere-chat-prompt.ts b/packages/cohere/src/convert-to-cohere-chat-prompt.ts index 42583a970320..b9cb4cf4d362 100644 --- a/packages/cohere/src/convert-to-cohere-chat-prompt.ts +++ b/packages/cohere/src/convert-to-cohere-chat-prompt.ts @@ -163,7 +163,7 @@ export async function convertToCohereChatPrompt( contentValue = output.value; break; case 'execution-denied': - contentValue = output.reason ?? 'Tool execution denied.'; + contentValue = output.reason ?? 'Tool call execution denied.'; break; case 'content': case 'json': diff --git a/packages/deepseek/src/chat/convert-to-deepseek-chat-messages.ts b/packages/deepseek/src/chat/convert-to-deepseek-chat-messages.ts index edd001990a60..91c0598f96ed 100644 --- a/packages/deepseek/src/chat/convert-to-deepseek-chat-messages.ts +++ b/packages/deepseek/src/chat/convert-to-deepseek-chat-messages.ts @@ -151,7 +151,7 @@ export function convertToDeepSeekChatMessages({ contentValue = output.value; break; case 'execution-denied': - contentValue = output.reason ?? 'Tool execution denied.'; + contentValue = output.reason ?? 'Tool call execution denied.'; break; case 'content': case 'json': diff --git a/packages/google/src/convert-to-google-generative-ai-messages.ts b/packages/google/src/convert-to-google-generative-ai-messages.ts index 87f27d8991d7..89a287c82c35 100644 --- a/packages/google/src/convert-to-google-generative-ai-messages.ts +++ b/packages/google/src/convert-to-google-generative-ai-messages.ts @@ -510,7 +510,7 @@ export function convertToGoogleGenerativeAIMessages( name: part.toolName, content: output.type === 'execution-denied' - ? (output.reason ?? 'Tool execution denied.') + ? (output.reason ?? 'Tool call execution denied.') : output.value, }, }, diff --git a/packages/groq/src/convert-to-groq-chat-messages.ts b/packages/groq/src/convert-to-groq-chat-messages.ts index de59bf936496..015adbf88eb4 100644 --- a/packages/groq/src/convert-to-groq-chat-messages.ts +++ b/packages/groq/src/convert-to-groq-chat-messages.ts @@ -118,7 +118,7 @@ export function convertToGroqChatMessages( contentValue = output.value; break; case 'execution-denied': - contentValue = output.reason ?? 'Tool execution denied.'; + contentValue = output.reason ?? 'Tool call execution denied.'; break; case 'content': case 'json': diff --git a/packages/mistral/src/convert-to-mistral-chat-messages.ts b/packages/mistral/src/convert-to-mistral-chat-messages.ts index 1c74a7ee8959..3c6914f8da61 100644 --- a/packages/mistral/src/convert-to-mistral-chat-messages.ts +++ b/packages/mistral/src/convert-to-mistral-chat-messages.ts @@ -134,7 +134,7 @@ export function convertToMistralChatMessages( contentValue = output.value; break; case 'execution-denied': - contentValue = output.reason ?? 'Tool execution denied.'; + contentValue = output.reason ?? 'Tool call execution denied.'; break; case 'content': case 'json': diff --git a/packages/open-responses/src/responses/convert-to-open-responses-input.ts b/packages/open-responses/src/responses/convert-to-open-responses-input.ts index ed23608ca5df..2e06c35c5d13 100644 --- a/packages/open-responses/src/responses/convert-to-open-responses-input.ts +++ b/packages/open-responses/src/responses/convert-to-open-responses-input.ts @@ -133,7 +133,7 @@ export async function convertToOpenResponsesInput({ contentValue = output.value; break; case 'execution-denied': - contentValue = output.reason ?? 'Tool execution denied.'; + contentValue = output.reason ?? 'Tool call execution denied.'; break; case 'json': case 'error-json': diff --git a/packages/openai-compatible/src/chat/convert-to-openai-compatible-chat-messages.ts b/packages/openai-compatible/src/chat/convert-to-openai-compatible-chat-messages.ts index 638ce2b0dd29..b4a34826347d 100644 --- a/packages/openai-compatible/src/chat/convert-to-openai-compatible-chat-messages.ts +++ b/packages/openai-compatible/src/chat/convert-to-openai-compatible-chat-messages.ts @@ -226,7 +226,7 @@ export function convertToOpenAICompatibleChatMessages( contentValue = output.value; break; case 'execution-denied': - contentValue = output.reason ?? 'Tool execution denied.'; + contentValue = output.reason ?? 'Tool call execution denied.'; break; case 'content': case 'json': diff --git a/packages/openai/src/chat/convert-to-openai-chat-messages.ts b/packages/openai/src/chat/convert-to-openai-chat-messages.ts index 7ad82fea920c..c2ee58cbd7e3 100644 --- a/packages/openai/src/chat/convert-to-openai-chat-messages.ts +++ b/packages/openai/src/chat/convert-to-openai-chat-messages.ts @@ -200,7 +200,7 @@ export function convertToOpenAIChatMessages({ contentValue = output.value; break; case 'execution-denied': - contentValue = output.reason ?? 'Tool execution denied.'; + contentValue = output.reason ?? 'Tool call execution denied.'; break; case 'content': case 'json': diff --git a/packages/openai/src/responses/convert-to-openai-responses-input.test.ts b/packages/openai/src/responses/convert-to-openai-responses-input.test.ts index 16efc3b05e5a..47d8b780e6d9 100644 --- a/packages/openai/src/responses/convert-to-openai-responses-input.test.ts +++ b/packages/openai/src/responses/convert-to-openai-responses-input.test.ts @@ -2332,6 +2332,39 @@ describe('convertToOpenAIResponsesInput', () => { `); }); + it('should convert execution-denied tool result to function_call_output', async () => { + const result = await convertToOpenAIResponsesInput({ + toolNameMapping: testToolNameMapping, + prompt: [ + { + role: 'tool', + content: [ + { + type: 'tool-result', + toolCallId: 'call_denied_123', + toolName: 'search', + output: { + type: 'execution-denied', + reason: 'User denied the tool execution', + }, + }, + ], + }, + ], + systemMessageMode: 'system', + providerOptionsName: 'openai', + store: true, + }); + + expect(result.input).toEqual([ + { + type: 'function_call_output', + call_id: 'call_denied_123', + output: 'User denied the tool execution', + }, + ]); + }); + it('should convert single tool result part with multipart that contains text', async () => { const result = await convertToOpenAIResponsesInput({ toolNameMapping: testToolNameMapping, @@ -3241,6 +3274,127 @@ describe('convertToOpenAIResponsesInput', () => { `); }); + it('should skip provider-executed execution-denied tool results in assistant messages', async () => { + const result = await convertToOpenAIResponsesInput({ + toolNameMapping: testToolNameMapping, + prompt: [ + { + role: 'assistant', + content: [ + { + type: 'text', + text: 'I need approval before running that tool.', + }, + { + type: 'tool-result', + toolCallId: 'ws_denied_123', + toolName: 'web_search', + output: { + type: 'execution-denied', + reason: 'User denied the tool execution', + }, + }, + { + type: 'text', + text: 'The tool was not run.', + }, + ], + }, + ], + systemMessageMode: 'system', + providerOptionsName: 'openai', + store: false, + }); + + expect(result).toEqual({ + input: [ + { + role: 'assistant', + content: [ + { + type: 'output_text', + text: 'I need approval before running that tool.', + }, + ], + id: undefined, + }, + { + role: 'assistant', + content: [ + { + type: 'output_text', + text: 'The tool was not run.', + }, + ], + id: undefined, + }, + ], + warnings: [], + }); + }); + + it('should skip json-wrapped execution-denied tool results in assistant messages', async () => { + const result = await convertToOpenAIResponsesInput({ + toolNameMapping: testToolNameMapping, + prompt: [ + { + role: 'assistant', + content: [ + { + type: 'text', + text: 'I need approval before running that tool.', + }, + { + type: 'tool-result', + toolCallId: 'ws_denied_json_123', + toolName: 'web_search', + output: { + type: 'json', + value: { + type: 'execution-denied', + reason: 'User denied the tool execution', + }, + }, + }, + { + type: 'text', + text: 'The tool was not run.', + }, + ], + }, + ], + systemMessageMode: 'system', + providerOptionsName: 'openai', + store: false, + }); + + expect(result).toEqual({ + input: [ + { + role: 'assistant', + content: [ + { + type: 'output_text', + text: 'I need approval before running that tool.', + }, + ], + id: undefined, + }, + { + role: 'assistant', + content: [ + { + type: 'output_text', + text: 'The tool was not run.', + }, + ], + id: undefined, + }, + ], + warnings: [], + }); + }); + describe('local shell', () => { it('should convert local shell tool call and result into item reference with store: true', async () => { const result = await convertToOpenAIResponsesInput({ @@ -4599,6 +4753,40 @@ describe('convertToOpenAIResponsesInput', () => { `); }); + it('should convert execution-denied custom tool result to custom_tool_call_output', async () => { + const result = await convertToOpenAIResponsesInput({ + toolNameMapping: testToolNameMapping, + prompt: [ + { + role: 'tool', + content: [ + { + type: 'tool-result', + toolCallId: 'call_custom_denied_001', + toolName: 'write_sql', + output: { + type: 'execution-denied', + reason: 'User denied the tool execution', + }, + }, + ], + }, + ], + systemMessageMode: 'system', + providerOptionsName: 'openai', + store: true, + customProviderToolNames, + }); + + expect(result.input).toEqual([ + { + type: 'custom_tool_call_output', + call_id: 'call_custom_denied_001', + output: 'User denied the tool execution', + }, + ]); + }); + it('should convert custom tool result content output', async () => { const result = await convertToOpenAIResponsesInput({ toolNameMapping: testToolNameMapping, diff --git a/packages/openai/src/responses/convert-to-openai-responses-input.ts b/packages/openai/src/responses/convert-to-openai-responses-input.ts index 707c03ca105a..73a0badcf54f 100644 --- a/packages/openai/src/responses/convert-to-openai-responses-input.ts +++ b/packages/openai/src/responses/convert-to-openai-responses-input.ts @@ -734,7 +734,7 @@ export async function convertToOpenAIResponsesInput({ outputValue = output.value; break; case 'execution-denied': - outputValue = output.reason ?? 'Tool execution denied.'; + outputValue = output.reason ?? 'Tool call execution denied.'; break; case 'json': case 'error-json': @@ -801,7 +801,7 @@ export async function convertToOpenAIResponsesInput({ contentValue = output.value; break; case 'execution-denied': - contentValue = output.reason ?? 'Tool execution denied.'; + contentValue = output.reason ?? 'Tool call execution denied.'; break; case 'json': case 'error-json': diff --git a/packages/xai/src/convert-to-xai-chat-messages.ts b/packages/xai/src/convert-to-xai-chat-messages.ts index 15827ac0ba47..c3cdbf653669 100644 --- a/packages/xai/src/convert-to-xai-chat-messages.ts +++ b/packages/xai/src/convert-to-xai-chat-messages.ts @@ -113,7 +113,7 @@ export function convertToXaiChatMessages(prompt: LanguageModelV3Prompt): { contentValue = output.value; break; case 'execution-denied': - contentValue = output.reason ?? 'Tool execution denied.'; + contentValue = output.reason ?? 'Tool call execution denied.'; break; case 'content': case 'json':