Skip to content

Commit e7b9eeb

Browse files
committed
fix: strip leading {} from streaming tool call arguments
When streaming, some models return an initial empty `{}` followed by the actual arguments, resulting in `{}{...}`. This fix detects and strips the leading `{}` prefix while preserving legitimate empty arguments.
1 parent 1900729 commit e7b9eeb

File tree

2 files changed

+58
-0
lines changed

2 files changed

+58
-0
lines changed

packages/agents-openai/src/openaiChatCompletionsStreaming.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,9 @@ export async function* convertChatCompletionsStreamToResponses(
138138
}
139139

140140
for (const function_call of Object.values(state.function_calls)) {
141+
if (function_call.arguments.startsWith('{}{')) {
142+
function_call.arguments = function_call.arguments.slice(2);
143+
}
141144
outputs.push(function_call);
142145
}
143146

packages/agents-openai/test/openaiChatCompletionsStreaming.test.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,4 +265,59 @@ describe('convertChatCompletionsStreamToResponses', () => {
265265
rawContent: [{ type: 'reasoning_text', text: 'foobar' }],
266266
});
267267
});
268+
269+
it('strips leading {} from tool call arguments when followed by real args', async () => {
270+
const resp = { id: 'r' } as any;
271+
272+
async function* stream() {
273+
yield makeChunk({
274+
tool_calls: [
275+
{ index: 0, id: 'call1', function: { name: 'fn', arguments: '{}' } },
276+
],
277+
});
278+
yield makeChunk({
279+
tool_calls: [{ index: 0, function: { arguments: '{"key":"value"}' } }],
280+
});
281+
}
282+
283+
const events: any[] = [];
284+
for await (const e of convertChatCompletionsStreamToResponses(
285+
resp,
286+
stream() as any,
287+
)) {
288+
events.push(e);
289+
}
290+
291+
const final = events[events.length - 1];
292+
const functionCall = final.response.output.find(
293+
(o: any) => o.type === 'function_call',
294+
);
295+
expect(functionCall.arguments).toBe('{"key":"value"}');
296+
});
297+
298+
it('preserves {} for legitimate empty tool call arguments', async () => {
299+
const resp = { id: 'r' } as any;
300+
301+
async function* stream() {
302+
yield makeChunk({
303+
tool_calls: [
304+
{ index: 0, id: 'call1', function: { name: 'fn', arguments: '{}' } },
305+
],
306+
});
307+
}
308+
309+
const events: any[] = [];
310+
for await (const e of convertChatCompletionsStreamToResponses(
311+
resp,
312+
stream() as any,
313+
)) {
314+
events.push(e);
315+
}
316+
317+
const final = events[events.length - 1];
318+
const functionCall = final.response.output.find(
319+
(o: any) => o.type === 'function_call',
320+
);
321+
expect(functionCall.arguments).toBe('{}');
322+
});
268323
});

0 commit comments

Comments
 (0)