Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fix-empty-streaming-tool-args.md
Original file line number Diff line number Diff line change
@@ -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.
3 changes: 3 additions & 0 deletions packages/agents-openai/src/openaiChatCompletionsStreaming.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,9 @@ export async function* convertChatCompletionsStreamToResponses(
}

for (const function_call of Object.values(state.function_calls)) {
if (function_call.arguments.startsWith('{}{')) {
function_call.arguments = function_call.arguments.slice(2);
}
outputs.push(function_call);
}

Expand Down
55 changes: 55 additions & 0 deletions packages/agents-openai/test/openaiChatCompletionsStreaming.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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('{}');
});
});