From 3a5fe76959ca633ced8bff007a60ad4f1cdaed78 Mon Sep 17 00:00:00 2001 From: cyphercodes <7407177+cyphercodes@users.noreply.github.com> Date: Wed, 29 Apr 2026 15:12:08 +0300 Subject: [PATCH] fix(responses): avoid parsing incomplete structured output --- src/lib/ResponsesParser.ts | 5 +- tests/lib/ResponsesParser.test.ts | 79 +++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 2 deletions(-) create mode 100644 tests/lib/ResponsesParser.test.ts diff --git a/src/lib/ResponsesParser.ts b/src/lib/ResponsesParser.ts index 50a078ee9c..41f1a3e852 100644 --- a/src/lib/ResponsesParser.ts +++ b/src/lib/ResponsesParser.ts @@ -64,12 +64,13 @@ export function parseResponse< Params extends ResponseCreateParamsBase, ParsedT = ExtractParsedContentFromParams, >(response: Response, params: Params): ParsedResponse { + const shouldParse = !response.status || response.status === 'completed'; const output: Array> = response.output.map( (item): ParsedResponseOutputItem => { if (item.type === 'function_call') { return { ...item, - parsed_arguments: parseToolCall(params, item), + parsed_arguments: shouldParse ? parseToolCall(params, item) : null, }; } if (item.type === 'message') { @@ -77,7 +78,7 @@ export function parseResponse< if (content.type === 'output_text') { return { ...content, - parsed: parseTextFormat(params, content.text), + parsed: shouldParse ? parseTextFormat(params, content.text) : null, }; } diff --git a/tests/lib/ResponsesParser.test.ts b/tests/lib/ResponsesParser.test.ts new file mode 100644 index 0000000000..740312f5c9 --- /dev/null +++ b/tests/lib/ResponsesParser.test.ts @@ -0,0 +1,79 @@ +import { parseResponse } from '../../src/lib/ResponsesParser'; +import type { Response, ResponseCreateParamsBase } from '../../src/resources/responses/responses'; + +const structuredTextParams = { + model: 'gpt-5.4-mini', + input: 'Good large pea', + text: { + format: { + type: 'json_schema', + name: 'pea_schema', + schema: { type: 'object' }, + }, + }, +} as ResponseCreateParamsBase; + +function makeResponse(status: Response['status'], text: string): Response { + return { + id: 'resp_123', + created_at: 0, + error: null, + incomplete_details: status === 'incomplete' ? { reason: 'max_output_tokens' } : null, + instructions: null, + metadata: null, + model: 'gpt-5.4-mini', + object: 'response', + output: [ + { + id: 'msg_123', + type: 'message', + role: 'assistant', + status, + content: [ + { + type: 'output_text', + annotations: [], + logprobs: [], + text, + }, + ], + }, + ], + output_text: text, + parallel_tool_calls: true, + temperature: null, + tool_choice: 'auto', + tools: [], + top_p: null, + status, + } as Response; +} + +describe('ResponsesParser', () => { + it('parses structured output for completed responses', () => { + const response = parseResponse( + makeResponse('completed', '{"size":"large","quality":"good"}'), + structuredTextParams, + ); + + expect(response.output_parsed).toEqual({ size: 'large', quality: 'good' }); + }); + + it('leaves incomplete structured output unparsed so incomplete_details remain inspectable', () => { + const response = parseResponse( + makeResponse('incomplete', '{"size":"large","quality":"good","pea_description":"unterminated'), + structuredTextParams, + ); + + expect(response.status).toBe('incomplete'); + expect(response.incomplete_details).toEqual({ reason: 'max_output_tokens' }); + expect(response.output_parsed).toBeNull(); + expect(response.output[0]?.type).toBe('message'); + if (response.output[0]?.type === 'message') { + expect(response.output[0].content[0]).toMatchObject({ + type: 'output_text', + parsed: null, + }); + } + }); +});