From 0343bb9ba751275958398f85d48b6220957e2899 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Wed, 5 Aug 2026 10:15:21 -0700 Subject: [PATCH] fix(providers): name the header phase on streaming OpenAI requests --- .../openai/core.transport-phase.test.ts | 14 ++++++ apps/sim/providers/openai/core.ts | 44 +++++++++++-------- 2 files changed, 40 insertions(+), 18 deletions(-) diff --git a/apps/sim/providers/openai/core.transport-phase.test.ts b/apps/sim/providers/openai/core.transport-phase.test.ts index 74459fc40e5..d0a27422a99 100644 --- a/apps/sim/providers/openai/core.transport-phase.test.ts +++ b/apps/sim/providers/openai/core.transport-phase.test.ts @@ -198,6 +198,20 @@ describe('OpenAI transport phase annotation', () => { expect(error.message.match(/phase=/g)).toHaveLength(1) }) + /** + * Streaming calls the request helper directly and never reaches `postResponses`, so a + * header stall on a chat or SSE run used to surface as the bare runtime string with no + * phase, elapsed time, or request id. + */ + it('names the header phase on a streaming request too', async () => { + const error = await run(vi.fn().mockRejectedValue(timeoutError()), { + stream: true, + }).catch((e) => e) + + expect(error.message).toContain('phase=awaiting-response-headers') + expect(error.message).toMatch(/elapsedMs=\d+/) + }) + it('leaves a healthy response entirely unaffected', async () => { const fetchMock = vi.fn().mockResolvedValue({ ok: true, diff --git a/apps/sim/providers/openai/core.ts b/apps/sim/providers/openai/core.ts index 739d6d21e87..77848c08937 100644 --- a/apps/sim/providers/openai/core.ts +++ b/apps/sim/providers/openai/core.ts @@ -410,6 +410,29 @@ export async function executeResponsesProviderRequest( let reasoningSummariesUnavailable = false + /** + * The single point every Responses request leaves through, so a stall waiting for + * headers is named on the streaming paths too — they call + * {@link fetchResponsesWithSummaryFallback} directly and never reach `postResponses`, + * which is where the annotation used to live. + */ + const postOnce = async ( + payload: Record, + abortSignal: AbortSignal | undefined, + startedAt: number + ): Promise => { + try { + return await fetchImpl(config.endpoint, { + method: 'POST', + headers: config.headers, + body: JSON.stringify(payload), + signal: abortSignal, + }) + } catch (error) { + throw annotateTransportFailure(error, 'awaiting-response-headers', startedAt) + } + } + const fetchResponsesWithSummaryFallback = async ( requestedBody: Record, startedAt: number, @@ -418,12 +441,7 @@ export async function executeResponsesProviderRequest( const body = reasoningSummariesUnavailable ? (stripReasoningSummary(requestedBody) ?? requestedBody) : requestedBody - const response = await fetchImpl(config.endpoint, { - method: 'POST', - headers: config.headers, - body: JSON.stringify(body), - signal: abortSignal, - }) + const response = await postOnce(body, abortSignal, startedAt) if (response.ok) return response const message = await parseErrorResponse(response, startedAt) @@ -439,12 +457,7 @@ export async function executeResponsesProviderRequest( `${config.providerLabel} rejected reasoning summaries (organization not verified); retrying without summary`, { model: config.modelName } ) - const retryResponse = await fetchImpl(config.endpoint, { - method: 'POST', - headers: config.headers, - body: JSON.stringify(strippedBody), - signal: abortSignal, - }) + const retryResponse = await postOnce(strippedBody, abortSignal, startedAt) if (!retryResponse.ok) { const retryMessage = await parseErrorResponse(retryResponse, startedAt) throw new Error( @@ -459,12 +472,7 @@ export async function executeResponsesProviderRequest( ): Promise => { const startedAt = Date.now() - let response: Response - try { - response = await fetchResponsesWithSummaryFallback(body, startedAt) - } catch (error) { - throw annotateTransportFailure(error, 'awaiting-response-headers', startedAt) - } + const response = await fetchResponsesWithSummaryFallback(body, startedAt) const responseMeta = { ...describeResponse(response), ttfbMs: Date.now() - startedAt }