diff --git a/.changeset/fix-empty-streaming-tool-args.md b/.changeset/fix-empty-streaming-tool-args.md new file mode 100644 index 00000000..7e431c88 --- /dev/null +++ b/.changeset/fix-empty-streaming-tool-args.md @@ -0,0 +1,5 @@ +--- +"@openai/agents-openai": patch +--- + +Fix streaming tool call arguments when providers like Bedrock return an initial empty `{}` followed by actual arguments, resulting in malformed `{}{...}` JSON. diff --git a/packages/agents-openai/src/openaiChatCompletionsStreaming.ts b/packages/agents-openai/src/openaiChatCompletionsStreaming.ts index e9d16a62..c35dcb29 100644 --- a/packages/agents-openai/src/openaiChatCompletionsStreaming.ts +++ b/packages/agents-openai/src/openaiChatCompletionsStreaming.ts @@ -138,6 +138,12 @@ export async function* convertChatCompletionsStreamToResponses( } for (const function_call of Object.values(state.function_calls)) { + // Some providers, such as Bedrock, may send two items: + // 1) an empty argument, and 2) the actual argument data. + // This is a workaround for that specific behavior. + if (function_call.arguments.startsWith('{}{')) { + function_call.arguments = function_call.arguments.slice(2); + } outputs.push(function_call); } diff --git a/packages/agents-openai/test/openaiChatCompletionsStreaming.test.ts b/packages/agents-openai/test/openaiChatCompletionsStreaming.test.ts index 8ee957b1..d4084ed2 100644 --- a/packages/agents-openai/test/openaiChatCompletionsStreaming.test.ts +++ b/packages/agents-openai/test/openaiChatCompletionsStreaming.test.ts @@ -265,4 +265,59 @@ describe('convertChatCompletionsStreamToResponses', () => { rawContent: [{ type: 'reasoning_text', text: 'foobar' }], }); }); + + it('strips leading {} from tool call arguments when followed by real args', async () => { + const resp = { id: 'r' } as any; + + async function* stream() { + yield makeChunk({ + tool_calls: [ + { index: 0, id: 'call1', function: { name: 'fn', arguments: '{}' } }, + ], + }); + yield makeChunk({ + tool_calls: [{ index: 0, function: { arguments: '{"key":"value"}' } }], + }); + } + + const events: any[] = []; + for await (const e of convertChatCompletionsStreamToResponses( + resp, + stream() as any, + )) { + events.push(e); + } + + const final = events[events.length - 1]; + const functionCall = final.response.output.find( + (o: any) => o.type === 'function_call', + ); + expect(functionCall.arguments).toBe('{"key":"value"}'); + }); + + it('preserves {} for legitimate empty tool call arguments', async () => { + const resp = { id: 'r' } as any; + + async function* stream() { + yield makeChunk({ + tool_calls: [ + { index: 0, id: 'call1', function: { name: 'fn', arguments: '{}' } }, + ], + }); + } + + const events: any[] = []; + for await (const e of convertChatCompletionsStreamToResponses( + resp, + stream() as any, + )) { + events.push(e); + } + + const final = events[events.length - 1]; + const functionCall = final.response.output.find( + (o: any) => o.type === 'function_call', + ); + expect(functionCall.arguments).toBe('{}'); + }); });