Przeglądaj źródła

fix(ai): handle incomplete responses without reasons (#38374)

Aiden Cline 3 tygodni temu
rodzic
commit
48bcbd09ef

+ 3 - 2
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"

+ 26 - 0
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", () =>