From b89ac7051e98525c9668d5ba4f435bdc3d5e77a9 Mon Sep 17 00:00:00 2001 From: Hayden Date: Mon, 13 Jul 2026 12:45:58 -0700 Subject: [PATCH] fix(runTools): sanitize replayed messages --- src/lib/AbstractChatCompletionRunner.ts | 44 +++++++++++++++++++- tests/lib/ChatCompletionRunFunctions.test.ts | 43 +------------------ 2 files changed, 44 insertions(+), 43 deletions(-) diff --git a/src/lib/AbstractChatCompletionRunner.ts b/src/lib/AbstractChatCompletionRunner.ts index 3d939d62a..494d6f207 100644 --- a/src/lib/AbstractChatCompletionRunner.ts +++ b/src/lib/AbstractChatCompletionRunner.ts @@ -5,6 +5,7 @@ import { uuid4 } from '../internal/utils/uuid'; import { isAutoParsableTool, parseChatCompletion } from '../lib/parser'; import type { ChatCompletion, + ChatCompletionAssistantMessageParam, ChatCompletionCreateParams, ChatCompletionMessage, ChatCompletionMessageFunctionToolCall, @@ -49,6 +50,47 @@ function normalizeToolCallIds(chatCompletion: ChatCompletion): void { } } +/** + * Parsed completions contain response-only and helper-only fields. Keep those + * on runner.messages for callers, but only replay valid request fields. + */ +function toRequestMessage(message: ChatCompletionMessageParam): ChatCompletionMessageParam { + if (!isAssistantMessage(message)) return message; + + const requestMessage: ChatCompletionAssistantMessageParam = { role: 'assistant' }; + + if (message.audio != null) requestMessage.audio = { id: message.audio.id }; + if (message.content !== undefined) requestMessage.content = message.content; + if (message.function_call != null) requestMessage.function_call = message.function_call; + if (message.name !== undefined) requestMessage.name = message.name; + if (message.refusal != null) requestMessage.refusal = message.refusal; + if (message.tool_calls !== undefined) { + requestMessage.tool_calls = message.tool_calls.map((toolCall) => { + if (toolCall.type === 'custom') { + return { + id: toolCall.id, + type: toolCall.type, + custom: { + input: toolCall.custom.input, + name: toolCall.custom.name, + }, + }; + } + + return { + id: toolCall.id, + type: toolCall.type, + function: { + arguments: toolCall.function.arguments, + name: toolCall.function.name, + }, + }; + }); + } + + return requestMessage; +} + export interface ChatCompletionRunnerContext { messages: ChatCompletionMessageParam[]; abort(): void; @@ -413,7 +455,7 @@ export class AbstractChatCompletionRunner< ...restParams, tool_choice, tools, - messages: [...this.messages], + messages: this.messages.map(toRequestMessage), }, options, ); diff --git a/tests/lib/ChatCompletionRunFunctions.test.ts b/tests/lib/ChatCompletionRunFunctions.test.ts index 1ded19d3a..f6103ea8d 100644 --- a/tests/lib/ChatCompletionRunFunctions.test.ts +++ b/tests/lib/ChatCompletionRunFunctions.test.ts @@ -644,6 +644,7 @@ describe('resource completions', () => { }); await handleRequest(async (request) => { + // Parsed response metadata stays on the runner but must not be sent back to the API. expect(request.messages).toEqual([ { role: 'user', @@ -652,8 +653,6 @@ describe('resource completions', () => { { role: 'assistant', content: null, - refusal: null, - parsed: null, tool_calls: [ { type: 'function', @@ -661,7 +660,6 @@ describe('resource completions', () => { function: { arguments: '', name: 'getWeather', - parsed_arguments: null, }, }, ], @@ -1281,8 +1279,6 @@ describe('resource completions', () => { { role: 'assistant', content: null, - parsed: null, - refusal: null, tool_calls: [ { type: 'function', @@ -1290,7 +1286,6 @@ describe('resource completions', () => { function: { arguments: '{"a": 1, "b": 2, "c": 3}', name: 'numProperties', - parsed_arguments: null, }, }, ], @@ -1436,8 +1431,6 @@ describe('resource completions', () => { { role: 'assistant', content: null, - parsed: null, - refusal: null, tool_calls: [ { type: 'function', @@ -1445,7 +1438,6 @@ describe('resource completions', () => { function: { arguments: '[{"a": 1, "b": 2, "c": 3}]', name: 'numProperties', - parsed_arguments: null, }, }, ], @@ -1495,8 +1487,6 @@ describe('resource completions', () => { { role: 'assistant', content: null, - parsed: null, - refusal: null, tool_calls: [ { type: 'function', @@ -1504,7 +1494,6 @@ describe('resource completions', () => { function: { arguments: '[{"a": 1, "b": 2, "c": 3}]', name: 'numProperties', - parsed_arguments: null, }, }, ], @@ -1517,8 +1506,6 @@ describe('resource completions', () => { { role: 'assistant', content: null, - parsed: null, - refusal: null, tool_calls: [ { type: 'function', @@ -1526,7 +1513,6 @@ describe('resource completions', () => { function: { arguments: '{"a": 1, "b": 2, "c": 3}', name: 'numProperties', - parsed_arguments: null, }, }, ], @@ -1757,8 +1743,6 @@ describe('resource completions', () => { { role: 'assistant', content: null, - parsed: null, - refusal: null, tool_calls: [ { type: 'function', @@ -1766,7 +1750,6 @@ describe('resource completions', () => { function: { arguments: '', name: 'get_weather', - parsed_arguments: null, }, }, ], @@ -1813,8 +1796,6 @@ describe('resource completions', () => { { role: 'assistant', content: null, - parsed: null, - refusal: null, tool_calls: [ { type: 'function', @@ -1822,7 +1803,6 @@ describe('resource completions', () => { function: { arguments: '', name: 'get_weather', - parsed_arguments: null, }, }, ], @@ -1835,8 +1815,6 @@ describe('resource completions', () => { { role: 'assistant', content: null, - parsed: null, - refusal: null, tool_calls: [ { type: 'function', @@ -1844,7 +1822,6 @@ describe('resource completions', () => { function: { arguments: '', name: 'getWeather', - parsed_arguments: null, }, }, ], @@ -2006,8 +1983,6 @@ describe('resource completions', () => { { role: 'assistant', content: null, - parsed: null, - refusal: null, tool_calls: [ { type: 'function', @@ -2133,8 +2108,6 @@ describe('resource completions', () => { { role: 'assistant', content: null, - parsed: null, - refusal: null, tool_calls: [ { type: 'function', @@ -2697,8 +2670,6 @@ describe('resource completions', () => { { role: 'assistant', content: null, - parsed: null, - refusal: null, tool_calls: [ { type: 'function', @@ -2820,8 +2791,6 @@ describe('resource completions', () => { { role: 'assistant', content: null, - parsed: null, - refusal: null, tool_calls: [ { type: 'function', @@ -2861,8 +2830,6 @@ describe('resource completions', () => { { role: 'assistant', content: null, - parsed: null, - refusal: null, tool_calls: [ { type: 'function', @@ -2882,8 +2849,6 @@ describe('resource completions', () => { { role: 'assistant', content: null, - parsed: null, - refusal: null, tool_calls: [ { type: 'function', @@ -3107,8 +3072,6 @@ describe('resource completions', () => { { role: 'assistant', content: null, - parsed: null, - refusal: null, tool_calls: [ { type: 'function', @@ -3161,8 +3124,6 @@ describe('resource completions', () => { { role: 'assistant', content: null, - parsed: null, - refusal: null, tool_calls: [ { type: 'function', @@ -3182,8 +3143,6 @@ describe('resource completions', () => { { role: 'assistant', content: null, - parsed: null, - refusal: null, tool_calls: [ { type: 'function',