diff --git a/packages/ai/src/protocols/openai-responses.ts b/packages/ai/src/protocols/openai-responses.ts index 87ca8c75e1a..2973acf0005 100644 --- a/packages/ai/src/protocols/openai-responses.ts +++ b/packages/ai/src/protocols/openai-responses.ts @@ -253,7 +253,7 @@ const OpenAIResponsesEvent = Schema.Struct({ Schema.Struct({ id: Schema.optional(Schema.String), service_tier: optionalNull(Schema.String), - incomplete_details: optionalNull(Schema.Struct({ reason: Schema.String })), + incomplete_details: optionalNull(Schema.Struct({ reason: Schema.optional(Schema.String) })), usage: optionalNull(OpenAIResponsesUsage), error: optionalNull(OpenAIResponsesErrorPayload), }), @@ -602,7 +602,8 @@ const mapUsage = (usage: OpenAIResponsesUsage | null | undefined) => { const mapFinishReason = (event: OpenAIResponsesEvent, hasFunctionCall: boolean): FinishReason => { const reason = event.response?.incomplete_details?.reason - if (reason === undefined || reason === null) return hasFunctionCall ? "tool-calls" : "stop" + if (reason === undefined || reason === null) + return hasFunctionCall ? "tool-calls" : event.type === "response.incomplete" ? "unknown" : "stop" if (reason === "max_output_tokens") return "length" if (reason === "content_filter") return "content-filter" return hasFunctionCall ? "tool-calls" : "unknown" diff --git a/packages/ai/test/provider/openai-responses.test.ts b/packages/ai/test/provider/openai-responses.test.ts index 11466a6896f..ffbd3294341 100644 --- a/packages/ai/test/provider/openai-responses.test.ts +++ b/packages/ai/test/provider/openai-responses.test.ts @@ -870,6 +870,32 @@ describe("OpenAI Responses route", () => { }), ) + it.effect("maps incomplete response reasons", () => + Effect.gen(function* () { + const generate = (incompleteDetails: object) => + LLMClient.generate(request).pipe( + Effect.provide( + fixedResponse( + sseEvents({ + type: "response.incomplete", + response: { id: "resp_incomplete", incomplete_details: incompleteDetails }, + }), + ), + ), + ) + + const length = yield* generate({ reason: "max_output_tokens" }) + const contentFilter = yield* generate({ reason: "content_filter" }) + const unknown = yield* generate({}) + + expect([length.finishReason, contentFilter.finishReason, unknown.finishReason]).toEqual([ + "length", + "content-filter", + "unknown", + ]) + }), + ) + // OpenAI's documented stream orders output text within one message item; no // provider-valid same-kind overlap is evidenced, so done boundaries close it. it.effect("closes sequential output messages before starting the next", () =>