Procházet zdrojové kódy

test(llm): lock event reducer laws (#34423)

Shoubhit Dash před 1 měsícem
rodič
revize
e5101d9651

+ 4 - 0
packages/llm/test/provider/anthropic-messages.test.ts

@@ -395,6 +395,10 @@ describe("Anthropic Messages route", () => {
       expect(response.events.find((event) => event.type === "reasoning-end")).toMatchObject({
         providerMetadata: { anthropic: { signature: "sig_1" } },
       })
+      expect(response.message.content).toEqual([
+        { type: "text", text: "Hello!" },
+        { type: "reasoning", text: "thinking", providerMetadata: { anthropic: { signature: "sig_1" } } },
+      ])
       expect(response.events.at(-1)).toMatchObject({
         type: "finish",
         reason: "stop",

+ 5 - 0
packages/llm/test/provider/openai-responses.test.ts

@@ -778,6 +778,11 @@ describe("OpenAI Responses route", () => {
         { type: "step-finish", index: 0, reason: "stop" },
         { type: "finish", reason: "stop" },
       ])
+      expect(response.events.filter((event) => event.type === "finish")).toHaveLength(1)
+      expect(response.message.content).toEqual([
+        { type: "reasoning", text: "thinking" },
+        { type: "text", text: "Hello" },
+      ])
     }),
   )
 

+ 41 - 3
packages/llm/test/response.test.ts

@@ -2,10 +2,11 @@ import { describe, expect, test } from "bun:test"
 import { LLMEvent, LLMResponse } from "../src"
 
 const reduce = (events: ReadonlyArray<LLMEvent>) => events.reduce(LLMResponse.reduce, LLMResponse.empty())
+const finishEvents = (events: ReadonlyArray<LLMEvent>) => events.filter(LLMEvent.is.finish)
 
 describe("LLMResponse reducer", () => {
   test("assembles interleaved reasoning and text with end metadata", () => {
-    const response = LLMResponse.fromEvents([
+    const events = [
       LLMEvent.reasoningStart({ id: "r1" }),
       LLMEvent.reasoningDelta({ id: "r1", text: "I should " }),
       LLMEvent.textStart({ id: "t1" }),
@@ -14,10 +15,23 @@ describe("LLMResponse reducer", () => {
       LLMEvent.textDelta({ id: "t1", text: "Answer" }),
       LLMEvent.textEnd({ id: "t1" }),
       LLMEvent.finish({ reason: "stop", usage: { outputTokens: 5 } }),
-    ])
+    ]
+    const response = LLMResponse.fromEvents(events)
 
     expect(response?.finishReason).toBe("stop")
     expect(response?.usage).toMatchObject({ outputTokens: 5 })
+    expect(response?.events).toEqual(events)
+    expect(response?.events.map((event) => event.type)).toEqual([
+      "reasoning-start",
+      "reasoning-delta",
+      "text-start",
+      "reasoning-delta",
+      "reasoning-end",
+      "text-delta",
+      "text-end",
+      "finish",
+    ])
+    expect(finishEvents(response?.events ?? [])).toHaveLength(1)
     expect(response?.message.content).toEqual([
       {
         type: "reasoning",
@@ -26,7 +40,6 @@ describe("LLMResponse reducer", () => {
       },
       { type: "text", text: "Answer" },
     ])
-    expect(response?.events).toHaveLength(8)
   })
 
   test("preserves partial content without completing a failed stream", () => {
@@ -36,6 +49,31 @@ describe("LLMResponse reducer", () => {
     expect(state.message.content).toEqual([{ type: "text", text: "partial" }])
   })
 
+  test("does not complete ended content without a terminal finish", () => {
+    const state = reduce([
+      LLMEvent.textStart({ id: "t1" }),
+      LLMEvent.textDelta({ id: "t1", text: "partial" }),
+      LLMEvent.textEnd({ id: "t1" }),
+    ])
+
+    expect(LLMResponse.complete(state)).toBeUndefined()
+    expect(state.message.content).toEqual([{ type: "text", text: "partial" }])
+  })
+
+  test("uses terminal usage when present and keeps prior usage when finish omits it", () => {
+    const withFinishUsage = LLMResponse.fromEvents([
+      LLMEvent.stepFinish({ index: 0, reason: "stop", usage: { inputTokens: 3 } }),
+      LLMEvent.finish({ reason: "stop", usage: { outputTokens: 2 } }),
+    ])
+    const withoutFinishUsage = LLMResponse.fromEvents([
+      LLMEvent.stepFinish({ index: 0, reason: "stop", usage: { inputTokens: 3 } }),
+      LLMEvent.finish({ reason: "stop" }),
+    ])
+
+    expect(withFinishUsage?.usage).toMatchObject({ outputTokens: 2 })
+    expect(withoutFinishUsage?.usage).toMatchObject({ inputTokens: 3 })
+  })
+
   test("assembles tool-call content only after the completed tool call event", () => {
     const pending = reduce([
       LLMEvent.toolInputStart({ id: "call_1", name: "lookup" }),