


























@@ -435,6 +435,216 @@ describe("openai transport stream", () => {
435435]);
436436});
437437438+it("collapses cumulative message snapshot items into one text block (#91959)", async () => {
439+const model = createAzureResponsesModel();
440+const output = createResponsesAssistantOutput(model);
441+const pushSpy = vi.fn();
442+const snapshot1 = "Scaled dot-product attention";
443+const snapshot2 = "Scaled dot-product attention divides by sqrt(d_k)";
444+const snapshot3 = "Scaled dot-product attention divides by sqrt(d_k) before softmax.";
445+const messageItem = (id: string, text: string) => ({
446+type: "message",
447+ id,
448+phase: "final_answer",
449+content: [{ type: "output_text", text }],
450+});
451+452+await testing.processResponsesStream(
453+streamChunks([
454+{
455+type: "response.output_item.added",
456+item: { type: "message", id: "msg_1", phase: "final_answer" },
457+},
458+{ type: "response.output_text.delta", delta: snapshot1 },
459+{ type: "response.output_item.done", item: messageItem("msg_1", snapshot1) },
460+{
461+type: "response.output_item.added",
462+item: { type: "message", id: "msg_2", phase: "final_answer" },
463+},
464+{ type: "response.output_item.done", item: messageItem("msg_2", snapshot2) },
465+{
466+type: "response.output_item.added",
467+item: { type: "message", id: "msg_3", phase: "final_answer" },
468+},
469+{ type: "response.output_item.done", item: messageItem("msg_3", snapshot3) },
470+{
471+type: "response.completed",
472+response: { id: "resp-snapshots", status: "completed" },
473+},
474+]),
475+output,
476+{ push: pushSpy },
477+model,
478+);
479+480+expect(output.content).toEqual([
481+{
482+type: "text",
483+text: snapshot3,
484+textSignature: '{"v":1,"id":"msg_3","phase":"final_answer"}',
485+},
486+]);
487+// Balanced lifecycle: one text_start, all events on index 0, and each
488+// collapsed snapshot re-ends the same block.
489+const textEvents = pushSpy.mock.calls
490+.map(([event]) => event as { type: string; contentIndex?: number })
491+.filter((event) => event.type.startsWith("text_"));
492+expect(textEvents.map((event) => [event.type, event.contentIndex])).toEqual([
493+["text_start", 0],
494+["text_delta", 0],
495+["text_end", 0],
496+["text_end", 0],
497+["text_end", 0],
498+]);
499+});
500+501+it("keeps prefix-nested message items separated by a tool call as separate blocks", async () => {
502+const model = createAzureResponsesModel();
503+const output = createResponsesAssistantOutput(model);
504+const messageEvents = (id: string, text: string) => [
505+{ type: "response.output_item.added", item: { type: "message", id } },
506+{
507+type: "response.output_item.done",
508+item: { type: "message", id, content: [{ type: "output_text", text }] },
509+},
510+];
511+512+await testing.processResponsesStream(
513+streamChunks([
514+ ...messageEvents("msg_1", "Done."),
515+{
516+type: "response.output_item.added",
517+item: {
518+type: "function_call",
519+id: "fc_1",
520+call_id: "call_1",
521+name: "write",
522+arguments: "{}",
523+},
524+},
525+{
526+type: "response.output_item.done",
527+item: {
528+type: "function_call",
529+id: "fc_1",
530+call_id: "call_1",
531+name: "write",
532+arguments: "{}",
533+},
534+},
535+ ...messageEvents("msg_2", "Done."),
536+{
537+type: "response.completed",
538+response: { id: "resp-tool-boundary", status: "completed" },
539+},
540+]),
541+output,
542+{ push: vi.fn() },
543+model,
544+);
545+546+// The post-tool message is a real reply, not a snapshot of the pre-tool one.
547+expect(output.content.map((block) => block.type)).toEqual(["text", "toolCall", "text"]);
548+expect(output.content[2]).toMatchObject({ type: "text", text: "Done." });
549+});
550+551+it("collapses cumulative message snapshots in completed-response backfill (#91959)", async () => {
552+const model = createAzureResponsesModel();
553+const output = createResponsesAssistantOutput(model);
554+555+await testing.processResponsesStream(
556+streamChunks([
557+{
558+type: "response.completed",
559+response: {
560+id: "resp-backfill-snapshots",
561+status: "completed",
562+output: [
563+{
564+type: "message",
565+id: "msg_1",
566+role: "assistant",
567+content: [{ type: "output_text", text: "The answer" }],
568+},
569+{
570+type: "message",
571+id: "msg_2",
572+role: "assistant",
573+content: [{ type: "output_text", text: "The answer is 42." }],
574+},
575+{
576+type: "message",
577+id: "msg_3",
578+role: "assistant",
579+content: [{ type: "output_text", text: "The answer" }],
580+},
581+],
582+},
583+},
584+]),
585+output,
586+{ push: vi.fn() },
587+model,
588+);
589+590+// msg_2 strictly extends msg_1 and collapses into it; msg_3 shrinks back
591+// and is an independently identified message, so it stays a real block.
592+expect(output.content).toEqual([
593+{
594+type: "text",
595+text: "The answer is 42.",
596+textSignature: '{"v":1,"id":"msg_2"}',
597+},
598+{
599+type: "text",
600+text: "The answer",
601+textSignature: '{"v":1,"id":"msg_3"}',
602+},
603+]);
604+});
605+606+it("keeps backfill message items separated by a reasoning item as distinct blocks", async () => {
607+const model = createAzureResponsesModel();
608+const output = createResponsesAssistantOutput(model);
609+610+await testing.processResponsesStream(
611+streamChunks([
612+{
613+type: "response.completed",
614+response: {
615+id: "resp-backfill-reasoning-boundary",
616+status: "completed",
617+output: [
618+{
619+type: "message",
620+id: "msg_1",
621+role: "assistant",
622+content: [{ type: "output_text", text: "Step one." }],
623+},
624+{ type: "reasoning", id: "rs_1", summary: [] },
625+{
626+type: "message",
627+id: "msg_2",
628+role: "assistant",
629+content: [{ type: "output_text", text: "Step one. Step two." }],
630+},
631+],
632+},
633+},
634+]),
635+output,
636+{ push: vi.fn() },
637+model,
638+);
639+640+// A reasoning item is a real boundary even in backfill: msg_2 must not
641+// collapse into msg_1 despite being a strict extension (mirrors streaming).
642+expect(output.content).toEqual([
643+{ type: "text", text: "Step one.", textSignature: '{"v":1,"id":"msg_1"}' },
644+{ type: "text", text: "Step one. Step two.", textSignature: '{"v":1,"id":"msg_2"}' },
645+]);
646+});
647+438648it("backfills Azure Responses completed function calls when item events are absent", async () => {
439649const model = createAzureResponsesModel();
440650const output = createResponsesAssistantOutput(model);
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。