diff --git a/packages/ai/src/providers/openai-responses.ts b/packages/ai/src/providers/openai-responses.ts index 2be20dd81..053bc06b3 100644 --- a/packages/ai/src/providers/openai-responses.ts +++ b/packages/ai/src/providers/openai-responses.ts @@ -594,7 +594,11 @@ const streamOpenAIResponsesOnce = ( error instanceof Error && /previous[ _]?response/i.test(error.message) && /zero[ _-]?data[ _-]?retention/i.test(error.message); - if (!zdrRejection && !isOpenAIResponsesStalePreviousResponseError(error)) { + const isPromptBlocked = + error instanceof Error && + ((error as { code?: string }).code === "invalid_prompt" || + /invalid_prompt|Request blocked/i.test(error.message)); + if (!zdrRejection && !isPromptBlocked && !isOpenAIResponsesStalePreviousResponseError(error)) { throw error; } // Server rejected the chain baseline: reset, count the failure (or diff --git a/packages/ai/test/openai-responses-stateful.test.ts b/packages/ai/test/openai-responses-stateful.test.ts index 8f6fb1b1a..8871f0961 100644 --- a/packages/ai/test/openai-responses-stateful.test.ts +++ b/packages/ai/test/openai-responses-stateful.test.ts @@ -230,6 +230,58 @@ describe("openai-responses stateful chaining", () => { expect(JSON.stringify(sentRequests[2]?.input)).toContain("First question"); expect(JSON.stringify(sentRequests[2]?.input)).toContain("Second question"); }); + it("retries a blocked invalid_prompt previous_response_id with the full transcript", async () => { + const sentRequests: Array> = []; + const fetchMock = vi.fn(async (_input: string | URL | Request, init?: RequestInit) => { + const request = JSON.parse(String(init?.body)) as Record; + sentRequests.push(request); + if (typeof request.previous_response_id === "string") { + return new Response( + JSON.stringify({ + error: { + message: "Request blocked.", + type: "invalid_request_error", + code: "invalid_prompt", + }, + }), + { status: 400, headers: { "content-type": "application/json" } }, + ); + } + return createStatefulSse(`Answer ${sentRequests.length}`, `resp_${sentRequests.length}`); + }) as FetchImpl; + const providerSessionState = new Map(); + const options = { + apiKey: "test-key", + sessionId: "stateful-blocked-session", + providerSessionState, + statefulResponses: true, + reasoning: "low" as const, + fetch: fetchMock, + }; + + const firstUser = { role: "user" as const, content: "First question", timestamp: 1000 }; + const firstResponse = await streamOpenAIResponses( + model, + { systemPrompt, messages: [firstUser] }, + options, + ).result(); + const secondResponse = await streamOpenAIResponses( + model, + { + systemPrompt, + messages: [firstUser, firstResponse, { role: "user", content: "Second question", timestamp: 1001 }], + }, + options, + ).result(); + + expect(secondResponse.stopReason).toBe("stop"); + expect(JSON.stringify(secondResponse.content)).toContain("Answer 3"); + expect(sentRequests).toHaveLength(3); + expect(sentRequests[1]?.previous_response_id).toBe("resp_1"); + expect(sentRequests[2]?.previous_response_id).toBeUndefined(); + expect(JSON.stringify(sentRequests[2]?.input)).toContain("First question"); + expect(JSON.stringify(sentRequests[2]?.input)).toContain("Second question"); + }); it("disables chaining for the session after repeated stale failures and stops forcing store", async () => { const sentRequests: Array> = [];