From a7ef0b8d850086bd61593f595acd8ebdea210de2 Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Fri, 16 Jan 2026 15:41:26 -0500 Subject: [PATCH 1/4] fix: integrate handleChatCompletion to save chat memories The handleChatCompletion function was implemented but never called from the chat handlers, causing memories to not be saved to the database. This fix integrates handleChatCompletion into both handleChatStream and handleChatGenerate to properly save user and assistant messages. Co-Authored-By: Claude Opus 4.5 --- lib/chat/__tests__/handleChatGenerate.test.ts | 229 +++++++++++++++--- lib/chat/__tests__/handleChatStream.test.ts | 228 +++++++++++++++-- lib/chat/handleChatGenerate.ts | 8 +- lib/chat/handleChatStream.ts | 13 +- 4 files changed, 412 insertions(+), 66 deletions(-) diff --git a/lib/chat/__tests__/handleChatGenerate.test.ts b/lib/chat/__tests__/handleChatGenerate.test.ts index 04b0a05d..92b71ae3 100644 --- a/lib/chat/__tests__/handleChatGenerate.test.ts +++ b/lib/chat/__tests__/handleChatGenerate.test.ts @@ -1,6 +1,13 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import { NextResponse } from "next/server"; +import { getApiKeyAccountId } from "@/lib/auth/getApiKeyAccountId"; +import { validateOverrideAccountId } from "@/lib/accounts/validateOverrideAccountId"; +import { setupChatRequest } from "@/lib/chat/setupChatRequest"; +import { handleChatCompletion } from "@/lib/chat/handleChatCompletion"; +import { generateText } from "ai"; +import { handleChatGenerate } from "../handleChatGenerate"; + // Mock all dependencies before importing the module under test vi.mock("@/lib/auth/getApiKeyAccountId", () => ({ getApiKeyAccountId: vi.fn(), @@ -26,26 +33,27 @@ vi.mock("@/lib/chat/setupChatRequest", () => ({ setupChatRequest: vi.fn(), })); +vi.mock("@/lib/chat/handleChatCompletion", () => ({ + handleChatCompletion: vi.fn(), +})); + vi.mock("ai", () => ({ generateText: vi.fn(), })); -import { getApiKeyAccountId } from "@/lib/auth/getApiKeyAccountId"; -import { validateOverrideAccountId } from "@/lib/accounts/validateOverrideAccountId"; -import { setupChatRequest } from "@/lib/chat/setupChatRequest"; -import { generateText } from "ai"; -import { handleChatGenerate } from "../handleChatGenerate"; - const mockGetApiKeyAccountId = vi.mocked(getApiKeyAccountId); const mockValidateOverrideAccountId = vi.mocked(validateOverrideAccountId); const mockSetupChatRequest = vi.mocked(setupChatRequest); +const mockHandleChatCompletion = vi.mocked(handleChatCompletion); const mockGenerateText = vi.mocked(generateText); // Helper to create mock NextRequest -function createMockRequest( - body: unknown, - headers: Record = {}, -): Request { +/** + * + * @param body + * @param headers + */ +function createMockRequest(body: unknown, headers: Record = {}): Request { return { json: () => Promise.resolve(body), headers: { @@ -58,6 +66,8 @@ function createMockRequest( describe("handleChatGenerate", () => { beforeEach(() => { vi.clearAllMocks(); + // Default mock for handleChatCompletion to return a resolved Promise + mockHandleChatCompletion.mockResolvedValue(); }); afterEach(() => { @@ -68,10 +78,7 @@ describe("handleChatGenerate", () => { it("returns 400 error when neither messages nor prompt is provided", async () => { mockGetApiKeyAccountId.mockResolvedValue("account-123"); - const request = createMockRequest( - { roomId: "room-123" }, - { "x-api-key": "test-key" }, - ); + const request = createMockRequest({ roomId: "room-123" }, { "x-api-key": "test-key" }); const result = await handleChatGenerate(request as any); @@ -122,10 +129,7 @@ describe("handleChatGenerate", () => { }, } as any); - const request = createMockRequest( - { prompt: "Hello, world!" }, - { "x-api-key": "valid-key" }, - ); + const request = createMockRequest({ prompt: "Hello, world!" }, { "x-api-key": "valid-key" }); const result = await handleChatGenerate(request as any); @@ -157,10 +161,7 @@ describe("handleChatGenerate", () => { } as any); const messages = [{ role: "user", content: "Hello" }]; - const request = createMockRequest( - { messages }, - { "x-api-key": "valid-key" }, - ); + const request = createMockRequest({ messages }, { "x-api-key": "valid-key" }); await handleChatGenerate(request as any); @@ -237,10 +238,7 @@ describe("handleChatGenerate", () => { response: { messages: [], headers: {}, body: null }, } as any); - const request = createMockRequest( - { prompt: "Hello" }, - { "x-api-key": "valid-key" }, - ); + const request = createMockRequest({ prompt: "Hello" }, { "x-api-key": "valid-key" }); const result = await handleChatGenerate(request as any); @@ -256,10 +254,7 @@ describe("handleChatGenerate", () => { mockGetApiKeyAccountId.mockResolvedValue("account-123"); mockSetupChatRequest.mockRejectedValue(new Error("Setup failed")); - const request = createMockRequest( - { prompt: "Hello" }, - { "x-api-key": "valid-key" }, - ); + const request = createMockRequest({ prompt: "Hello" }, { "x-api-key": "valid-key" }); const result = await handleChatGenerate(request as any); @@ -284,10 +279,7 @@ describe("handleChatGenerate", () => { mockGenerateText.mockRejectedValue(new Error("Generation failed")); - const request = createMockRequest( - { prompt: "Hello" }, - { "x-api-key": "valid-key" }, - ); + const request = createMockRequest({ prompt: "Hello" }, { "x-api-key": "valid-key" }); const result = await handleChatGenerate(request as any); @@ -336,4 +328,173 @@ describe("handleChatGenerate", () => { ); }); }); + + describe("chat completion handling", () => { + it("calls handleChatCompletion after text generation", async () => { + mockGetApiKeyAccountId.mockResolvedValue("account-123"); + + const mockResponseMessages = [ + { + id: "resp-1", + role: "assistant", + parts: [{ type: "text", text: "Hello!" }], + }, + ]; + + mockSetupChatRequest.mockResolvedValue({ + model: "gpt-4", + instructions: "test", + system: "test", + messages: [], + experimental_generateMessageId: vi.fn(), + tools: {}, + providerOptions: {}, + } as any); + + mockGenerateText.mockResolvedValue({ + text: "Hello!", + finishReason: "stop", + usage: { promptTokens: 10, completionTokens: 20 }, + response: { messages: mockResponseMessages, headers: {}, body: null }, + } as any); + + mockHandleChatCompletion.mockResolvedValue(); + + const messages = [{ id: "msg-1", role: "user", parts: [{ type: "text", text: "Hi" }] }]; + const request = createMockRequest( + { messages, roomId: "room-123" }, + { "x-api-key": "valid-key" }, + ); + + await handleChatGenerate(request as any); + + expect(mockHandleChatCompletion).toHaveBeenCalledWith( + expect.objectContaining({ + messages, + roomId: "room-123", + accountId: "account-123", + }), + mockResponseMessages, + ); + }); + + it("passes artistId to handleChatCompletion when provided", async () => { + mockGetApiKeyAccountId.mockResolvedValue("account-123"); + + const mockResponseMessages = [ + { + id: "resp-1", + role: "assistant", + parts: [{ type: "text", text: "Hello!" }], + }, + ]; + + mockSetupChatRequest.mockResolvedValue({ + model: "gpt-4", + instructions: "test", + system: "test", + messages: [], + experimental_generateMessageId: vi.fn(), + tools: {}, + providerOptions: {}, + } as any); + + mockGenerateText.mockResolvedValue({ + text: "Hello!", + finishReason: "stop", + usage: { promptTokens: 10, completionTokens: 20 }, + response: { messages: mockResponseMessages, headers: {}, body: null }, + } as any); + + mockHandleChatCompletion.mockResolvedValue(); + + const messages = [{ id: "msg-1", role: "user", parts: [{ type: "text", text: "Hi" }] }]; + const request = createMockRequest( + { messages, roomId: "room-123", artistId: "artist-456" }, + { "x-api-key": "valid-key" }, + ); + + await handleChatGenerate(request as any); + + expect(mockHandleChatCompletion).toHaveBeenCalledWith( + expect.objectContaining({ + artistId: "artist-456", + }), + mockResponseMessages, + ); + }); + + it("does not throw when handleChatCompletion fails (graceful handling)", async () => { + mockGetApiKeyAccountId.mockResolvedValue("account-123"); + + mockSetupChatRequest.mockResolvedValue({ + model: "gpt-4", + instructions: "test", + system: "test", + messages: [], + experimental_generateMessageId: vi.fn(), + tools: {}, + providerOptions: {}, + } as any); + + mockGenerateText.mockResolvedValue({ + text: "Hello!", + finishReason: "stop", + usage: { promptTokens: 10, completionTokens: 20 }, + response: { + messages: [{ id: "resp-1", role: "assistant", parts: [] }], + headers: {}, + body: null, + }, + } as any); + + // Make handleChatCompletion throw an error + mockHandleChatCompletion.mockRejectedValue(new Error("Completion handling failed")); + + const request = createMockRequest({ prompt: "Hello" }, { "x-api-key": "valid-key" }); + + // Should still return 200 - completion handling failure should not affect response + const result = await handleChatGenerate(request as any); + expect(result.status).toBe(200); + }); + + it("calls handleChatCompletion even when validation skips it for missing roomId", async () => { + mockGetApiKeyAccountId.mockResolvedValue("account-123"); + + const mockResponseMessages = [ + { + id: "resp-1", + role: "assistant", + parts: [{ type: "text", text: "Hello!" }], + }, + ]; + + mockSetupChatRequest.mockResolvedValue({ + model: "gpt-4", + instructions: "test", + system: "test", + messages: [], + experimental_generateMessageId: vi.fn(), + tools: {}, + providerOptions: {}, + } as any); + + mockGenerateText.mockResolvedValue({ + text: "Hello!", + finishReason: "stop", + usage: { promptTokens: 10, completionTokens: 20 }, + response: { messages: mockResponseMessages, headers: {}, body: null }, + } as any); + + mockHandleChatCompletion.mockResolvedValue(); + + // No roomId provided + const request = createMockRequest({ prompt: "Hello" }, { "x-api-key": "valid-key" }); + + await handleChatGenerate(request as any); + + // handleChatCompletion should still be called (it handles room creation internally) + expect(mockHandleChatCompletion).toHaveBeenCalled(); + }); + }); }); diff --git a/lib/chat/__tests__/handleChatStream.test.ts b/lib/chat/__tests__/handleChatStream.test.ts index b78918e3..6a4e556b 100644 --- a/lib/chat/__tests__/handleChatStream.test.ts +++ b/lib/chat/__tests__/handleChatStream.test.ts @@ -1,6 +1,13 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import { NextResponse } from "next/server"; +import { getApiKeyAccountId } from "@/lib/auth/getApiKeyAccountId"; +import { validateOverrideAccountId } from "@/lib/accounts/validateOverrideAccountId"; +import { setupChatRequest } from "@/lib/chat/setupChatRequest"; +import { handleChatCompletion } from "@/lib/chat/handleChatCompletion"; +import { createUIMessageStream, createUIMessageStreamResponse } from "ai"; +import { handleChatStream } from "../handleChatStream"; + // Mock all dependencies before importing the module under test vi.mock("@/lib/auth/getApiKeyAccountId", () => ({ getApiKeyAccountId: vi.fn(), @@ -26,28 +33,29 @@ vi.mock("@/lib/chat/setupChatRequest", () => ({ setupChatRequest: vi.fn(), })); +vi.mock("@/lib/chat/handleChatCompletion", () => ({ + handleChatCompletion: vi.fn(), +})); + vi.mock("ai", () => ({ createUIMessageStream: vi.fn(), createUIMessageStreamResponse: vi.fn(), })); -import { getApiKeyAccountId } from "@/lib/auth/getApiKeyAccountId"; -import { validateOverrideAccountId } from "@/lib/accounts/validateOverrideAccountId"; -import { setupChatRequest } from "@/lib/chat/setupChatRequest"; -import { createUIMessageStream, createUIMessageStreamResponse } from "ai"; -import { handleChatStream } from "../handleChatStream"; - const mockGetApiKeyAccountId = vi.mocked(getApiKeyAccountId); const mockValidateOverrideAccountId = vi.mocked(validateOverrideAccountId); const mockSetupChatRequest = vi.mocked(setupChatRequest); +const mockHandleChatCompletion = vi.mocked(handleChatCompletion); const mockCreateUIMessageStream = vi.mocked(createUIMessageStream); const mockCreateUIMessageStreamResponse = vi.mocked(createUIMessageStreamResponse); // Helper to create mock NextRequest -function createMockRequest( - body: unknown, - headers: Record = {}, -): Request { +/** + * + * @param body + * @param headers + */ +function createMockRequest(body: unknown, headers: Record = {}): Request { return { json: () => Promise.resolve(body), headers: { @@ -60,6 +68,8 @@ function createMockRequest( describe("handleChatStream", () => { beforeEach(() => { vi.clearAllMocks(); + // Default mock for handleChatCompletion to return a resolved Promise + mockHandleChatCompletion.mockResolvedValue(); }); afterEach(() => { @@ -70,10 +80,7 @@ describe("handleChatStream", () => { it("returns 400 error when neither messages nor prompt is provided", async () => { mockGetApiKeyAccountId.mockResolvedValue("account-123"); - const request = createMockRequest( - { roomId: "room-123" }, - { "x-api-key": "test-key" }, - ); + const request = createMockRequest({ roomId: "room-123" }, { "x-api-key": "test-key" }); const result = await handleChatStream(request as any); @@ -124,10 +131,7 @@ describe("handleChatStream", () => { const mockResponse = new Response(mockStream); mockCreateUIMessageStreamResponse.mockReturnValue(mockResponse); - const request = createMockRequest( - { prompt: "Hello, world!" }, - { "x-api-key": "valid-key" }, - ); + const request = createMockRequest({ prompt: "Hello, world!" }, { "x-api-key": "valid-key" }); const result = await handleChatStream(request as any); @@ -166,10 +170,7 @@ describe("handleChatStream", () => { mockCreateUIMessageStreamResponse.mockReturnValue(new Response(mockStream)); const messages = [{ role: "user", content: "Hello" }]; - const request = createMockRequest( - { messages }, - { "x-api-key": "valid-key" }, - ); + const request = createMockRequest({ messages }, { "x-api-key": "valid-key" }); await handleChatStream(request as any); @@ -236,10 +237,7 @@ describe("handleChatStream", () => { mockGetApiKeyAccountId.mockResolvedValue("account-123"); mockSetupChatRequest.mockRejectedValue(new Error("Setup failed")); - const request = createMockRequest( - { prompt: "Hello" }, - { "x-api-key": "valid-key" }, - ); + const request = createMockRequest({ prompt: "Hello" }, { "x-api-key": "valid-key" }); const result = await handleChatStream(request as any); @@ -294,4 +292,182 @@ describe("handleChatStream", () => { ); }); }); + + describe("chat completion handling", () => { + it("calls handleChatCompletion after streaming completes", async () => { + mockGetApiKeyAccountId.mockResolvedValue("account-123"); + + const mockResponseMessages = [ + { + id: "resp-1", + role: "assistant", + parts: [{ type: "text", text: "Hello!" }], + }, + ]; + + const mockAgent = { + stream: vi.fn().mockResolvedValue({ + toUIMessageStream: vi.fn().mockReturnValue(new ReadableStream()), + usage: Promise.resolve({ inputTokens: 100, outputTokens: 50 }), + responseMessages: mockResponseMessages, + }), + tools: {}, + }; + + mockSetupChatRequest.mockResolvedValue({ + agent: mockAgent, + model: "gpt-4", + instructions: "test", + system: "test", + messages: [], + experimental_generateMessageId: vi.fn(), + tools: {}, + providerOptions: {}, + } as any); + + // Capture the execute callback and run it + let capturedExecute: ((options: { writer: { merge: () => void } }) => Promise) | null = + null; + mockCreateUIMessageStream.mockImplementation( + (options: { execute: typeof capturedExecute }) => { + capturedExecute = options.execute; + return new ReadableStream(); + }, + ); + mockCreateUIMessageStreamResponse.mockReturnValue(new Response(new ReadableStream())); + mockHandleChatCompletion.mockResolvedValue(); + + const messages = [{ id: "msg-1", role: "user", parts: [{ type: "text", text: "Hi" }] }]; + const request = createMockRequest( + { messages, roomId: "room-123" }, + { "x-api-key": "valid-key" }, + ); + + await handleChatStream(request as any); + + // Execute the captured callback to simulate stream completion + if (capturedExecute) { + await capturedExecute({ writer: { merge: vi.fn() } }); + } + + expect(mockHandleChatCompletion).toHaveBeenCalledWith( + expect.objectContaining({ + messages, + roomId: "room-123", + accountId: "account-123", + }), + mockResponseMessages, + ); + }); + + it("passes artistId to handleChatCompletion when provided", async () => { + mockGetApiKeyAccountId.mockResolvedValue("account-123"); + + const mockResponseMessages = [ + { + id: "resp-1", + role: "assistant", + parts: [{ type: "text", text: "Hello!" }], + }, + ]; + + const mockAgent = { + stream: vi.fn().mockResolvedValue({ + toUIMessageStream: vi.fn().mockReturnValue(new ReadableStream()), + usage: Promise.resolve({ inputTokens: 100, outputTokens: 50 }), + responseMessages: mockResponseMessages, + }), + tools: {}, + }; + + mockSetupChatRequest.mockResolvedValue({ + agent: mockAgent, + model: "gpt-4", + instructions: "test", + system: "test", + messages: [], + experimental_generateMessageId: vi.fn(), + tools: {}, + providerOptions: {}, + } as any); + + let capturedExecute: ((options: { writer: { merge: () => void } }) => Promise) | null = + null; + mockCreateUIMessageStream.mockImplementation( + (options: { execute: typeof capturedExecute }) => { + capturedExecute = options.execute; + return new ReadableStream(); + }, + ); + mockCreateUIMessageStreamResponse.mockReturnValue(new Response(new ReadableStream())); + mockHandleChatCompletion.mockResolvedValue(); + + const messages = [{ id: "msg-1", role: "user", parts: [{ type: "text", text: "Hi" }] }]; + const request = createMockRequest( + { messages, roomId: "room-123", artistId: "artist-456" }, + { "x-api-key": "valid-key" }, + ); + + await handleChatStream(request as any); + + if (capturedExecute) { + await capturedExecute({ writer: { merge: vi.fn() } }); + } + + expect(mockHandleChatCompletion).toHaveBeenCalledWith( + expect.objectContaining({ + artistId: "artist-456", + }), + mockResponseMessages, + ); + }); + + it("does not throw when handleChatCompletion fails (graceful handling)", async () => { + mockGetApiKeyAccountId.mockResolvedValue("account-123"); + + const mockAgent = { + stream: vi.fn().mockResolvedValue({ + toUIMessageStream: vi.fn().mockReturnValue(new ReadableStream()), + usage: Promise.resolve({ inputTokens: 100, outputTokens: 50 }), + responseMessages: [{ id: "resp-1", role: "assistant", parts: [] }], + }), + tools: {}, + }; + + mockSetupChatRequest.mockResolvedValue({ + agent: mockAgent, + model: "gpt-4", + instructions: "test", + system: "test", + messages: [], + experimental_generateMessageId: vi.fn(), + tools: {}, + providerOptions: {}, + } as any); + + let capturedExecute: ((options: { writer: { merge: () => void } }) => Promise) | null = + null; + mockCreateUIMessageStream.mockImplementation( + (options: { execute: typeof capturedExecute }) => { + capturedExecute = options.execute; + return new ReadableStream(); + }, + ); + mockCreateUIMessageStreamResponse.mockReturnValue(new Response(new ReadableStream())); + + // Make handleChatCompletion throw an error + mockHandleChatCompletion.mockRejectedValue(new Error("Completion handling failed")); + + const request = createMockRequest({ prompt: "Hello" }, { "x-api-key": "valid-key" }); + + // Should not throw + const result = await handleChatStream(request as any); + expect(result).toBeInstanceOf(Response); + + // Execute callback should not throw either + if (capturedExecute) { + await expect(capturedExecute({ writer: { merge: vi.fn() } })).resolves.toBeUndefined(); + } + }); + }); }); diff --git a/lib/chat/handleChatGenerate.ts b/lib/chat/handleChatGenerate.ts index d708bcff..fc5341be 100644 --- a/lib/chat/handleChatGenerate.ts +++ b/lib/chat/handleChatGenerate.ts @@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from "next/server"; import { generateText } from "ai"; import { validateChatRequest } from "./validateChatRequest"; import { setupChatRequest } from "./setupChatRequest"; +import { handleChatCompletion } from "./handleChatCompletion"; import { getCorsHeaders } from "@/lib/networking/getCorsHeaders"; /** @@ -28,8 +29,11 @@ export async function handleChatGenerate(request: NextRequest): Promise { + // Silently catch - handleChatCompletion handles its own error reporting + }); return NextResponse.json( { diff --git a/lib/chat/handleChatStream.ts b/lib/chat/handleChatStream.ts index 396a66ec..3e07b87c 100644 --- a/lib/chat/handleChatStream.ts +++ b/lib/chat/handleChatStream.ts @@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from "next/server"; import { createUIMessageStream, createUIMessageStreamResponse } from "ai"; import { validateChatRequest } from "./validateChatRequest"; import { setupChatRequest } from "./setupChatRequest"; +import { handleChatCompletion } from "./handleChatCompletion"; import { getCorsHeaders } from "@/lib/networking/getCorsHeaders"; import generateUUID from "@/lib/uuid/generateUUID"; @@ -30,14 +31,18 @@ export async function handleChatStream(request: NextRequest): Promise const stream = createUIMessageStream({ originalMessages: body.messages, generateId: generateUUID, - execute: async (options) => { + execute: async options => { const { writer } = options; const result = await agent.stream(chatConfig); writer.merge(result.toUIMessageStream()); - // Note: Credit handling and chat completion handling will be added - // as part of the handleChatCredits and handleChatCompletion migrations + + // Handle post-completion tasks (room creation, memory storage, notifications) + // Errors are handled gracefully within handleChatCompletion + handleChatCompletion(body, result.responseMessages).catch(() => { + // Silently catch - handleChatCompletion handles its own error reporting + }); }, - onError: (e) => { + onError: e => { console.error("/api/chat onError:", e); return JSON.stringify({ status: "error", From 23797878d7c095451b45d81696784fdd9653238a Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Fri, 16 Jan 2026 15:50:19 -0500 Subject: [PATCH 2/4] fix: construct UIMessage from generateText result for type compatibility generateText returns ResponseMessage[] which lacks id and parts properties. Construct a proper UIMessage from the result.text to pass to handleChatCompletion. Co-Authored-By: Claude Opus 4.5 --- lib/chat/__tests__/handleChatGenerate.test.ts | 34 ++++++++----------- lib/chat/handleChatGenerate.ts | 13 +++++-- 2 files changed, 25 insertions(+), 22 deletions(-) diff --git a/lib/chat/__tests__/handleChatGenerate.test.ts b/lib/chat/__tests__/handleChatGenerate.test.ts index 92b71ae3..1a90d497 100644 --- a/lib/chat/__tests__/handleChatGenerate.test.ts +++ b/lib/chat/__tests__/handleChatGenerate.test.ts @@ -333,14 +333,6 @@ describe("handleChatGenerate", () => { it("calls handleChatCompletion after text generation", async () => { mockGetApiKeyAccountId.mockResolvedValue("account-123"); - const mockResponseMessages = [ - { - id: "resp-1", - role: "assistant", - parts: [{ type: "text", text: "Hello!" }], - }, - ]; - mockSetupChatRequest.mockResolvedValue({ model: "gpt-4", instructions: "test", @@ -355,7 +347,7 @@ describe("handleChatGenerate", () => { text: "Hello!", finishReason: "stop", usage: { promptTokens: 10, completionTokens: 20 }, - response: { messages: mockResponseMessages, headers: {}, body: null }, + response: { messages: [], headers: {}, body: null }, } as any); mockHandleChatCompletion.mockResolvedValue(); @@ -374,21 +366,18 @@ describe("handleChatGenerate", () => { roomId: "room-123", accountId: "account-123", }), - mockResponseMessages, + expect.arrayContaining([ + expect.objectContaining({ + role: "assistant", + parts: [{ type: "text", text: "Hello!" }], + }), + ]), ); }); it("passes artistId to handleChatCompletion when provided", async () => { mockGetApiKeyAccountId.mockResolvedValue("account-123"); - const mockResponseMessages = [ - { - id: "resp-1", - role: "assistant", - parts: [{ type: "text", text: "Hello!" }], - }, - ]; - mockSetupChatRequest.mockResolvedValue({ model: "gpt-4", instructions: "test", @@ -403,7 +392,7 @@ describe("handleChatGenerate", () => { text: "Hello!", finishReason: "stop", usage: { promptTokens: 10, completionTokens: 20 }, - response: { messages: mockResponseMessages, headers: {}, body: null }, + response: { messages: [], headers: {}, body: null }, } as any); mockHandleChatCompletion.mockResolvedValue(); @@ -420,7 +409,12 @@ describe("handleChatGenerate", () => { expect.objectContaining({ artistId: "artist-456", }), - mockResponseMessages, + expect.arrayContaining([ + expect.objectContaining({ + role: "assistant", + parts: [{ type: "text", text: "Hello!" }], + }), + ]), ); }); diff --git a/lib/chat/handleChatGenerate.ts b/lib/chat/handleChatGenerate.ts index fc5341be..c1190293 100644 --- a/lib/chat/handleChatGenerate.ts +++ b/lib/chat/handleChatGenerate.ts @@ -1,9 +1,10 @@ import { NextRequest, NextResponse } from "next/server"; -import { generateText } from "ai"; +import { generateText, type UIMessage } from "ai"; import { validateChatRequest } from "./validateChatRequest"; import { setupChatRequest } from "./setupChatRequest"; import { handleChatCompletion } from "./handleChatCompletion"; import { getCorsHeaders } from "@/lib/networking/getCorsHeaders"; +import generateUUID from "@/lib/uuid/generateUUID"; /** * Handles a non-streaming chat generate request. @@ -29,9 +30,17 @@ export async function handleChatGenerate(request: NextRequest): Promise { + handleChatCompletion(body, [assistantMessage]).catch(() => { // Silently catch - handleChatCompletion handles its own error reporting }); From b9aab839ebc6249cb74df19d92c4a66e85f6e53f Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Fri, 16 Jan 2026 15:54:14 -0500 Subject: [PATCH 3/4] fix: remove createdAt property not in UIMessage type Co-Authored-By: Claude Opus 4.5 --- lib/chat/handleChatGenerate.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/chat/handleChatGenerate.ts b/lib/chat/handleChatGenerate.ts index c1190293..6d5f119f 100644 --- a/lib/chat/handleChatGenerate.ts +++ b/lib/chat/handleChatGenerate.ts @@ -35,7 +35,6 @@ export async function handleChatGenerate(request: NextRequest): Promise Date: Fri, 16 Jan 2026 15:58:08 -0500 Subject: [PATCH 4/4] fix: construct UIMessage from stream result.text instead of responseMessages StreamTextResult does not have responseMessages property. Use result.text to construct the UIMessage for handleChatCompletion. Co-Authored-By: Claude Opus 4.5 --- lib/chat/__tests__/handleChatStream.test.ts | 36 +++++++++------------ lib/chat/handleChatStream.ts | 12 +++++-- 2 files changed, 25 insertions(+), 23 deletions(-) diff --git a/lib/chat/__tests__/handleChatStream.test.ts b/lib/chat/__tests__/handleChatStream.test.ts index 6a4e556b..147cd4a5 100644 --- a/lib/chat/__tests__/handleChatStream.test.ts +++ b/lib/chat/__tests__/handleChatStream.test.ts @@ -297,19 +297,11 @@ describe("handleChatStream", () => { it("calls handleChatCompletion after streaming completes", async () => { mockGetApiKeyAccountId.mockResolvedValue("account-123"); - const mockResponseMessages = [ - { - id: "resp-1", - role: "assistant", - parts: [{ type: "text", text: "Hello!" }], - }, - ]; - const mockAgent = { stream: vi.fn().mockResolvedValue({ toUIMessageStream: vi.fn().mockReturnValue(new ReadableStream()), usage: Promise.resolve({ inputTokens: 100, outputTokens: 50 }), - responseMessages: mockResponseMessages, + text: Promise.resolve("Hello!"), }), tools: {}, }; @@ -356,26 +348,23 @@ describe("handleChatStream", () => { roomId: "room-123", accountId: "account-123", }), - mockResponseMessages, + expect.arrayContaining([ + expect.objectContaining({ + role: "assistant", + parts: [{ type: "text", text: "Hello!" }], + }), + ]), ); }); it("passes artistId to handleChatCompletion when provided", async () => { mockGetApiKeyAccountId.mockResolvedValue("account-123"); - const mockResponseMessages = [ - { - id: "resp-1", - role: "assistant", - parts: [{ type: "text", text: "Hello!" }], - }, - ]; - const mockAgent = { stream: vi.fn().mockResolvedValue({ toUIMessageStream: vi.fn().mockReturnValue(new ReadableStream()), usage: Promise.resolve({ inputTokens: 100, outputTokens: 50 }), - responseMessages: mockResponseMessages, + text: Promise.resolve("Hello!"), }), tools: {}, }; @@ -418,7 +407,12 @@ describe("handleChatStream", () => { expect.objectContaining({ artistId: "artist-456", }), - mockResponseMessages, + expect.arrayContaining([ + expect.objectContaining({ + role: "assistant", + parts: [{ type: "text", text: "Hello!" }], + }), + ]), ); }); @@ -429,7 +423,7 @@ describe("handleChatStream", () => { stream: vi.fn().mockResolvedValue({ toUIMessageStream: vi.fn().mockReturnValue(new ReadableStream()), usage: Promise.resolve({ inputTokens: 100, outputTokens: 50 }), - responseMessages: [{ id: "resp-1", role: "assistant", parts: [] }], + text: Promise.resolve("Hello!"), }), tools: {}, }; diff --git a/lib/chat/handleChatStream.ts b/lib/chat/handleChatStream.ts index 3e07b87c..b3c9611a 100644 --- a/lib/chat/handleChatStream.ts +++ b/lib/chat/handleChatStream.ts @@ -1,5 +1,5 @@ import { NextRequest, NextResponse } from "next/server"; -import { createUIMessageStream, createUIMessageStreamResponse } from "ai"; +import { createUIMessageStream, createUIMessageStreamResponse, type UIMessage } from "ai"; import { validateChatRequest } from "./validateChatRequest"; import { setupChatRequest } from "./setupChatRequest"; import { handleChatCompletion } from "./handleChatCompletion"; @@ -36,9 +36,17 @@ export async function handleChatStream(request: NextRequest): Promise const result = await agent.stream(chatConfig); writer.merge(result.toUIMessageStream()); + // Construct UIMessage from streaming result for handleChatCompletion + const text = await result.text; + const assistantMessage: UIMessage = { + id: generateUUID(), + role: "assistant", + parts: [{ type: "text", text }], + }; + // Handle post-completion tasks (room creation, memory storage, notifications) // Errors are handled gracefully within handleChatCompletion - handleChatCompletion(body, result.responseMessages).catch(() => { + handleChatCompletion(body, [assistantMessage]).catch(() => { // Silently catch - handleChatCompletion handles its own error reporting }); },