@node-llm/orm 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -68,7 +68,8 @@ export class Chat extends BaseChat {
68
68
 
69
69
  const coreChat = llmInstance.chat(model || undefined, {
70
70
  messages: history,
71
- ...this.localOptions
71
+ ...this.localOptions,
72
+ middlewares: this.customMiddlewares
72
73
  }) as any;
73
74
 
74
75
  // Register tools
@@ -333,17 +334,39 @@ export class Chat extends BaseChat {
333
334
  }
334
335
  }
335
336
 
337
+ /**
338
+ * Helper to find the correct table property in the prisma client.
339
+ * Prisma usually camelCases model names (e.g., AssistantChat -> assistantChat),
340
+ * but mapping can vary based on configuration.
341
+ */
342
+ function getTable(prisma: any, tableName: string): any {
343
+ if (prisma[tableName]) return prisma[tableName];
344
+
345
+ // Try case-insensitive match if not found directly
346
+ const keys = Object.keys(prisma).filter((k) => !k.startsWith("$") && !k.startsWith("_"));
347
+ const match = keys.find((k) => k.toLowerCase() === tableName.toLowerCase());
348
+
349
+ if (match) return prisma[match];
350
+
351
+ // If still not found, search for the model name if it's different from the property name
352
+ // but for now, we'll just throw a clear error
353
+ throw new Error(
354
+ `[@node-llm/orm] Prisma table "${tableName}" not found. Available tables: ${keys.join(", ")}`
355
+ );
356
+ }
357
+
336
358
  /**
337
359
  * Convenience method to create a new chat session.
338
360
  */
339
361
  export async function createChat<T = Record<string, any>>(
340
- prisma: PrismaClient,
362
+ prisma: any,
341
363
  llm: NodeLLMCore,
342
364
  options: ChatOptions & { tableNames?: TableNames } & T = {} as any
343
365
  ): Promise<Chat> {
344
366
  const chatTable = options.tableNames?.chat || "llmChat";
345
367
 
346
368
  // Extract known options so we don't double-pass them or pass them incorrectly
369
+ // runtime options should NOT be persisted to DB
347
370
  const {
348
371
  model,
349
372
  provider,
@@ -352,16 +375,32 @@ export async function createChat<T = Record<string, any>>(
352
375
  tableNames: _tableNames,
353
376
  debug: _debug,
354
377
  persistence: _persistence,
378
+ middlewares: _middlewares,
379
+ maxToolCalls: _maxToolCalls,
380
+ thinking: _thinking,
381
+ temperature: _temperature,
382
+ maxTokens: _maxTokens,
383
+ headers: _headers,
384
+ requestTimeout: _requestTimeout,
385
+ params: _params,
355
386
  ...extras
356
387
  } = options;
357
388
 
358
- const record = await (prisma as any)[chatTable].create({
389
+ if (options.debug) {
390
+ const keys = Object.keys(prisma).filter((k) => !k.startsWith("$") && !k.startsWith("_"));
391
+ console.log(
392
+ `[@node-llm/orm] createChat: table=${chatTable}, availableTables=${keys.join(", ")}`
393
+ );
394
+ }
395
+
396
+ const table = getTable(prisma, chatTable);
397
+ const record = await table.create({
359
398
  data: {
360
399
  model,
361
400
  provider,
362
401
  instructions,
363
402
  metadata: metadata ?? null,
364
- ...extras
403
+ ...(extras as any)
365
404
  }
366
405
  });
367
406
 
@@ -372,16 +411,25 @@ export async function createChat<T = Record<string, any>>(
372
411
  * Convenience method to load an existing chat session.
373
412
  */
374
413
  export async function loadChat(
375
- prisma: PrismaClient,
414
+ prisma: any,
376
415
  llm: NodeLLMCore,
377
416
  chatId: string,
378
- options: ChatOptions & { tableNames?: TableNames } = {}
417
+ options: ChatOptions & { tableNames?: TableNames; debug?: boolean } = {}
379
418
  ): Promise<Chat | null> {
380
419
  const chatTable = options.tableNames?.chat || "llmChat";
381
- const record = await (prisma as any)[chatTable].findUnique({
420
+
421
+ if (options.debug) {
422
+ const keys = Object.keys(prisma).filter((k) => !k.startsWith("$") && !k.startsWith("_"));
423
+ console.log(`[@node-llm/orm] loadChat: table=${chatTable}, availableTables=${keys.join(", ")}`);
424
+ }
425
+
426
+ const table = getTable(prisma, chatTable);
427
+ const record = await table.findUnique({
382
428
  where: { id: chatId }
383
429
  });
384
430
 
385
431
  if (!record) return null;
432
+
433
+ // Reconstruct chat with options from DB or manual overrides if needed
386
434
  return new Chat(prisma, llm, record, options, options.tableNames);
387
435
  }
@@ -4,7 +4,7 @@
4
4
  * Prisma adapter for NodeLLM ORM.
5
5
  * Provides automatic persistence of chats, messages, tool calls, and API requests.
6
6
  *
7
- * @example
7
+ * @example Chat API (low-level)
8
8
  * ```typescript
9
9
  * import { PrismaClient } from '@prisma/client';
10
10
  * import { createLLM } from '@node-llm/core';
@@ -21,7 +21,38 @@
21
21
  * const response = await chat.ask('Hello!');
22
22
  * console.log(response.content);
23
23
  * ```
24
+ *
25
+ * @example AgentSession API (recommended for agents)
26
+ * ```typescript
27
+ * import { Agent } from '@node-llm/core';
28
+ * import { createAgentSession, loadAgentSession } from '@node-llm/orm/prisma';
29
+ *
30
+ * class SupportAgent extends Agent {
31
+ * static model = 'gpt-4.1';
32
+ * static instructions = 'You are a helpful support agent.';
33
+ * }
34
+ *
35
+ * // Create new session
36
+ * const session = await createAgentSession(prisma, llm, SupportAgent, {
37
+ * metadata: { userId: 'user_123' }
38
+ * });
39
+ * await session.ask('Hello!');
40
+ *
41
+ * // Resume later (Code Wins - model/tools from class, history from DB)
42
+ * const session = await loadAgentSession(prisma, llm, SupportAgent, sessionId);
43
+ * await session.ask('Continue our conversation');
44
+ * ```
24
45
  */
25
46
 
47
+ // Chat API
26
48
  export { Chat, createChat, loadChat } from "./Chat.js";
27
- export type { ChatRecord, MessageRecord, ChatOptions, TableNames } from "./Chat.js";
49
+ export type { ChatRecord, MessageRecord, ChatOptions } from "./Chat.js";
50
+
51
+ // AgentSession API
52
+ export { AgentSession, createAgentSession, loadAgentSession } from "./AgentSession.js";
53
+ export type {
54
+ AgentSessionRecord,
55
+ CreateAgentSessionOptions,
56
+ LoadAgentSessionOptions,
57
+ TableNames // Export from AgentSession which includes agentSession key
58
+ } from "./AgentSession.js";
package/src/index.ts CHANGED
@@ -23,13 +23,33 @@
23
23
  * await chat.ask('Hello!');
24
24
  * ```
25
25
  *
26
+ * ## Agent Sessions (Recommended for Agents)
27
+ *
28
+ * ```typescript
29
+ * import { Agent } from '@node-llm/core';
30
+ * import { createAgentSession, loadAgentSession } from '@node-llm/orm/prisma';
31
+ *
32
+ * class SupportAgent extends Agent {
33
+ * static model = 'gpt-4.1';
34
+ * static instructions = 'You are a helpful support agent.';
35
+ * }
36
+ *
37
+ * // Create and persist
38
+ * const session = await createAgentSession(prisma, llm, SupportAgent);
39
+ * await session.ask('Hello!');
40
+ *
41
+ * // Resume later (Code Wins - model/tools from class, history from DB)
42
+ * const session = await loadAgentSession(prisma, llm, SupportAgent, sessionId);
43
+ * ```
44
+ *
26
45
  * ## Adapters
27
46
  *
28
47
  * - `@node-llm/orm/prisma` - Prisma adapter (recommended)
29
48
  *
30
49
  * ## Schema
31
50
  *
32
- * The ORM tracks four core entities:
51
+ * The ORM tracks five core entities:
52
+ * - **AgentSession** - Links Agent class to persistent Chat (v0.5.0+)
33
53
  * - **Chat** - Session container (model, provider, instructions)
34
54
  * - **Message** - User/Assistant conversation history
35
55
  * - **ToolCall** - Tool executions (name, arguments, results)
@@ -0,0 +1,204 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import { Agent, Tool, NodeLLM } from "@node-llm/core";
3
+ import { createAgentSession, loadAgentSession } from "../src/adapters/prisma/AgentSession";
4
+
5
+ // --- Mocks ---
6
+
7
+ // Mock Prisma Client
8
+ const mockPrisma = {
9
+ llmChat: {
10
+ create: vi.fn(),
11
+ findUnique: vi.fn()
12
+ },
13
+ llmAgentSession: {
14
+ create: vi.fn(),
15
+ findUnique: vi.fn()
16
+ },
17
+ llmMessage: {
18
+ create: vi.fn(),
19
+ findMany: vi.fn(),
20
+ update: vi.fn(),
21
+ delete: vi.fn()
22
+ }
23
+ };
24
+
25
+ // Mock LLM
26
+ const createMockChat = () => {
27
+ const mockChat = {
28
+ withInstructions: vi.fn().mockReturnThis(),
29
+ withTools: vi.fn().mockReturnThis(),
30
+ withSchema: vi.fn().mockReturnThis(),
31
+ ask: vi.fn().mockResolvedValue({
32
+ content: "Response",
33
+ meta: {},
34
+ usage: { input_tokens: 10, output_tokens: 5 }
35
+ }),
36
+ stream: vi.fn(),
37
+ history: [],
38
+ totalUsage: { input_tokens: 0, output_tokens: 0 },
39
+ modelId: "mock-model",
40
+ // Hook methods required by Agent constructor
41
+ beforeRequest: vi.fn().mockReturnThis(),
42
+ onToolCallStart: vi.fn().mockReturnThis(),
43
+ onToolCallEnd: vi.fn().mockReturnThis(),
44
+ onToolCallError: vi.fn().mockReturnThis(),
45
+ onEndMessage: vi.fn().mockReturnThis()
46
+ };
47
+ return mockChat;
48
+ };
49
+
50
+ const mockLlm = {
51
+ chat: vi.fn().mockImplementation(() => createMockChat())
52
+ } as unknown as typeof NodeLLM;
53
+
54
+ // --- Test Classes ---
55
+
56
+ class TestAgent extends Agent {
57
+ static model = "gpt-4-test";
58
+ static instructions = "Test instructions";
59
+ }
60
+
61
+ describe("AgentSession", () => {
62
+ beforeEach(() => {
63
+ vi.clearAllMocks();
64
+ });
65
+
66
+ describe("createAgentSession", () => {
67
+ it("creates a new session with correct metadata", async () => {
68
+ // Setup mocks
69
+ mockPrisma.llmChat.create.mockResolvedValue({ id: "chat-123" });
70
+ mockPrisma.llmAgentSession.create.mockResolvedValue({
71
+ id: "session-123",
72
+ chatId: "chat-123",
73
+ agentClass: "TestAgent",
74
+ metadata: { userId: "user-1" }
75
+ });
76
+
77
+ const session = await createAgentSession(mockPrisma as any, mockLlm, TestAgent, {
78
+ metadata: { userId: "user-1" }
79
+ });
80
+
81
+ // Verify DB calls
82
+ expect(mockPrisma.llmChat.create).toHaveBeenCalledWith(
83
+ expect.objectContaining({
84
+ data: expect.objectContaining({
85
+ model: "gpt-4-test",
86
+ instructions: "Test instructions"
87
+ })
88
+ })
89
+ );
90
+
91
+ expect(mockPrisma.llmAgentSession.create).toHaveBeenCalledWith(
92
+ expect.objectContaining({
93
+ data: expect.objectContaining({
94
+ agentClass: "TestAgent",
95
+ chatId: "chat-123",
96
+ metadata: { userId: "user-1" }
97
+ })
98
+ })
99
+ );
100
+
101
+ // Verify Session Object
102
+ expect(session.id).toBe("session-123");
103
+ expect(session.metadata).toEqual({ userId: "user-1" });
104
+ expect(session.agentClass).toBe("TestAgent");
105
+ });
106
+ });
107
+
108
+ describe("loadAgentSession", () => {
109
+ it("loads an existing session and injects history", async () => {
110
+ // Setup Mocks
111
+ mockPrisma.llmAgentSession.findUnique.mockResolvedValue({
112
+ id: "session-123",
113
+ chatId: "chat-123",
114
+ agentClass: "TestAgent",
115
+ metadata: { userId: "user-1" }
116
+ });
117
+
118
+ mockPrisma.llmMessage.findMany.mockResolvedValue([
119
+ { role: "user", content: "Hello" },
120
+ { role: "assistant", content: "Hi there" }
121
+ ]);
122
+
123
+ const session = await loadAgentSession(mockPrisma as any, mockLlm, TestAgent, "session-123");
124
+
125
+ expect(session).not.toBeNull();
126
+ expect(session?.id).toBe("session-123");
127
+
128
+ // Verify history injection (implicit via mock setup, would check agent internals in real integration)
129
+ // Implementation detail: The Agent constructor is called with { messages: [...] }
130
+ // We can verify this by checking if the agent property exists and works
131
+
132
+ expect(mockPrisma.llmMessage.findMany).toHaveBeenCalledWith(
133
+ expect.objectContaining({
134
+ where: { chatId: "chat-123" },
135
+ orderBy: { createdAt: "asc" }
136
+ })
137
+ );
138
+ });
139
+
140
+ it("throws error on agent class mismatch", async () => {
141
+ mockPrisma.llmAgentSession.findUnique.mockResolvedValue({
142
+ id: "session-123",
143
+ agentClass: "OtherAgent", // Mismatch
144
+ chatId: "chat-123"
145
+ });
146
+
147
+ await expect(
148
+ loadAgentSession(mockPrisma as any, mockLlm, TestAgent, "session-123")
149
+ ).rejects.toThrow("Agent class mismatch");
150
+ });
151
+ });
152
+
153
+ describe("ask", () => {
154
+ it("persists user and assistant messages", async () => {
155
+ // Setup Session
156
+ const sessionRecord = {
157
+ id: "session-123",
158
+ chatId: "chat-123",
159
+ agentClass: "TestAgent",
160
+ metadata: {},
161
+ createdAt: new Date(),
162
+ updatedAt: new Date()
163
+ };
164
+
165
+ // Mock AgentSession manually to test .ask()
166
+ // But simpler to use createAgentSession mock return if we could,
167
+ // here we instantiate directly or via factory.
168
+
169
+ mockPrisma.llmChat.create.mockResolvedValue({ id: "chat-123" });
170
+ mockPrisma.llmAgentSession.create.mockResolvedValue(sessionRecord);
171
+
172
+ const session = await createAgentSession(mockPrisma as any, mockLlm, TestAgent);
173
+
174
+ // Mock message creation
175
+ mockPrisma.llmMessage.create
176
+ .mockResolvedValueOnce({ id: "msg-user" }) // User message
177
+ .mockResolvedValueOnce({ id: "msg-asst" }); // Assistant placeholder
178
+
179
+ mockPrisma.llmMessage.update.mockResolvedValue({
180
+ id: "msg-asst",
181
+ content: "Response",
182
+ role: "assistant"
183
+ });
184
+
185
+ await session.ask("Hello");
186
+
187
+ // Verify persistence
188
+ expect(mockPrisma.llmMessage.create).toHaveBeenCalledTimes(2);
189
+ expect(mockPrisma.llmMessage.create).toHaveBeenNthCalledWith(
190
+ 1,
191
+ expect.objectContaining({
192
+ data: expect.objectContaining({ role: "user", content: "Hello" })
193
+ })
194
+ );
195
+
196
+ expect(mockPrisma.llmMessage.update).toHaveBeenCalledWith(
197
+ expect.objectContaining({
198
+ where: { id: "msg-asst" },
199
+ data: expect.objectContaining({ content: "Response" })
200
+ })
201
+ );
202
+ });
203
+ });
204
+ });
@@ -0,0 +1,116 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import { PrismaClient } from "@prisma/client";
3
+ import { NodeLLMCore, Agent } from "@node-llm/core";
4
+ import { createAgentSession, loadAgentSession } from "../src/adapters/prisma/AgentSession.js";
5
+
6
+ // Mock Prisma
7
+ const mockChatTable = {
8
+ create: vi.fn(),
9
+ findUnique: vi.fn(),
10
+ update: vi.fn()
11
+ };
12
+
13
+ const mockSessionTable = {
14
+ create: vi.fn(),
15
+ findUnique: vi.fn()
16
+ };
17
+
18
+ const mockMessageTable = {
19
+ create: vi.fn(),
20
+ findMany: vi.fn(),
21
+ delete: vi.fn(),
22
+ update: vi.fn()
23
+ };
24
+
25
+ const prisma = {
26
+ llmChat: mockChatTable,
27
+ llmAgentSession: mockSessionTable,
28
+ llmMessage: mockMessageTable
29
+ } as unknown as PrismaClient;
30
+
31
+ // Mock Agent & LLM
32
+ class TestAgent extends Agent {
33
+ static override model = "agent-model";
34
+ static override instructions = "agent-instructions";
35
+ }
36
+
37
+ const mockChat = {
38
+ withInstructions: vi.fn().mockReturnThis(),
39
+ withTools: vi.fn().mockReturnThis(),
40
+ withSchema: vi.fn().mockReturnThis(),
41
+ beforeRequest: vi.fn().mockReturnThis(),
42
+ onToolCallStart: vi.fn().mockReturnThis(),
43
+ onToolCallEnd: vi.fn().mockReturnThis(),
44
+ onToolCallError: vi.fn().mockReturnThis(),
45
+ onEndMessage: vi.fn().mockReturnThis(),
46
+ ask: vi.fn(),
47
+ messages: [],
48
+ modelId: "agent-model"
49
+ };
50
+
51
+ const llm = {
52
+ chat: vi.fn(() => mockChat)
53
+ } as unknown as NodeLLMCore;
54
+
55
+ describe("AgentSession - Code Wins Sovereignty", () => {
56
+ beforeEach(() => {
57
+ vi.clearAllMocks();
58
+ });
59
+
60
+ it("should prioritize Agent class instructions over DB instructions when loading a session", async () => {
61
+ const sessionId = "sess_123";
62
+ const chatId = "chat_456";
63
+
64
+ // 1. Mock DB returning DIFFERENT instructions than the class
65
+ mockSessionTable.findUnique.mockResolvedValue({
66
+ id: sessionId,
67
+ chatId: chatId,
68
+ agentClass: "TestAgent"
69
+ });
70
+
71
+ mockChatTable.findUnique.mockResolvedValue({
72
+ id: chatId,
73
+ model: "db-model", // DB says db-model
74
+ instructions: "db-instructions" // DB says db-instructions
75
+ });
76
+
77
+ mockMessageTable.findMany.mockResolvedValue([]);
78
+
79
+ // 2. Load the session
80
+ const session = await loadAgentSession(prisma, llm, TestAgent, sessionId);
81
+
82
+ expect(session).toBeDefined();
83
+
84
+ // 3. Verify Agent was instantiated with the correct LLM and history (empty here)
85
+ // The Agent constructor calls llm.chat(model, options)
86
+ expect(llm.chat).toHaveBeenCalledWith("agent-model", expect.any(Object));
87
+
88
+ // 4. Verify instructions applied to chat came from TestAgent.instructions
89
+ expect(mockChat.withInstructions).toHaveBeenCalledWith("agent-instructions");
90
+ expect(mockChat.withInstructions).not.toHaveBeenCalledWith("db-instructions");
91
+ });
92
+
93
+ it("should prioritize Agent class model over DB model when creating a session", async () => {
94
+ mockChatTable.create.mockResolvedValue({ id: "chat_789" });
95
+ mockSessionTable.create.mockResolvedValue({
96
+ id: "sess_789",
97
+ chatId: "chat_789",
98
+ agentClass: "TestAgent"
99
+ });
100
+
101
+ // 1. Create a session
102
+ await createAgentSession(prisma, llm, TestAgent);
103
+
104
+ // 2. Verify chat record was created with Agent class properties
105
+ expect(mockChatTable.create).toHaveBeenCalledWith({
106
+ data: expect.objectContaining({
107
+ model: "agent-model",
108
+ instructions: "agent-instructions"
109
+ })
110
+ });
111
+
112
+ // 3. Verify the live agent instance also uses these
113
+ expect(llm.chat).toHaveBeenCalledWith("agent-model", expect.any(Object));
114
+ expect(mockChat.withInstructions).toHaveBeenCalledWith("agent-instructions");
115
+ });
116
+ });
@@ -0,0 +1,137 @@
1
+ import { describe, it, expect, beforeEach, vi } from "vitest";
2
+ import { createChat } from "../src/adapters/prisma/index.js";
3
+ import { Middleware } from "@node-llm/core";
4
+
5
+ // Mock Prisma Client
6
+ const createMockPrisma = () => {
7
+ const messages: any[] = [];
8
+ const chats: any[] = [];
9
+
10
+ const tables: any = {
11
+ chat: {
12
+ create: vi.fn(async ({ data }) => {
13
+ const chat = { id: "chat-123", ...data, createdAt: new Date(), updatedAt: new Date() };
14
+ chats.push(chat);
15
+ return chat;
16
+ })
17
+ },
18
+ message: {
19
+ create: vi.fn(async ({ data }) => {
20
+ const message = { id: `msg-${messages.length}`, ...data, createdAt: new Date() };
21
+ messages.push(message);
22
+ return message;
23
+ }),
24
+ findMany: vi.fn(async ({ where }) => {
25
+ let filtered = messages.filter((m) => m.chatId === where.chatId);
26
+ if (where.id?.notIn) {
27
+ filtered = filtered.filter((m) => !where.id.notIn.includes(m.id));
28
+ }
29
+ return filtered;
30
+ }),
31
+ update: vi.fn(async ({ where, data }) => {
32
+ const message = messages.find((m) => m.id === where.id);
33
+ if (message) {
34
+ Object.assign(message, data);
35
+ }
36
+ return message;
37
+ }),
38
+ delete: vi.fn()
39
+ },
40
+ toolCall: {
41
+ create: vi.fn(),
42
+ update: vi.fn()
43
+ },
44
+ request: {
45
+ create: vi.fn()
46
+ },
47
+ _messages: messages
48
+ };
49
+
50
+ return new Proxy(tables, {
51
+ get(target, prop) {
52
+ if (prop in target) return target[prop];
53
+ if (typeof prop === "string") {
54
+ if (prop.toLowerCase().includes("chat")) return target.chat;
55
+ if (prop.toLowerCase().includes("message")) return target.message;
56
+ if (prop.toLowerCase().includes("toolcall")) return target.toolCall;
57
+ if (prop.toLowerCase().includes("request")) return target.request;
58
+ }
59
+ return undefined;
60
+ }
61
+ });
62
+ };
63
+
64
+ // Mock NodeLLM with middleware support
65
+ const createMockLLM = () => {
66
+ let capturedMiddlewares: Middleware[] = [];
67
+
68
+ const mockChat: any = {
69
+ system: vi.fn(() => mockChat),
70
+ withTools: vi.fn(() => mockChat),
71
+ onToolCallStart: vi.fn(() => mockChat),
72
+ onToolCallEnd: vi.fn(() => mockChat),
73
+ afterResponse: vi.fn(() => mockChat),
74
+ onNewMessage: vi.fn(() => mockChat),
75
+ onEndMessage: vi.fn(() => mockChat),
76
+ beforeRequest: vi.fn(() => mockChat),
77
+ ask: vi.fn(async () => {
78
+ return {
79
+ content: "Test response",
80
+ meta: { model: "gpt-4o-mini", provider: "openai" },
81
+ usage: { input_tokens: 10, output_tokens: 5, total_tokens: 15 },
82
+ model: "gpt-4o-mini",
83
+ provider: "openai"
84
+ };
85
+ })
86
+ };
87
+
88
+ const llm: any = {
89
+ chat: vi.fn((model, options) => {
90
+ if (options?.middlewares) {
91
+ capturedMiddlewares = options.middlewares;
92
+ }
93
+ return mockChat;
94
+ }),
95
+ withProvider: vi.fn(() => llm),
96
+ getMiddlewares: () => capturedMiddlewares
97
+ };
98
+
99
+ return llm;
100
+ };
101
+
102
+ describe("ORM Middleware Integration", () => {
103
+ let mockPrisma: any;
104
+ let mockLLM: any;
105
+
106
+ beforeEach(() => {
107
+ mockPrisma = createMockPrisma();
108
+ mockLLM = createMockLLM();
109
+ });
110
+
111
+ it("should pass middlewares to core Chat instance", async () => {
112
+ const middleware: Middleware = {
113
+ name: "TestMiddleware",
114
+ onRequest: vi.fn()
115
+ };
116
+
117
+ const chat = await createChat(mockPrisma, mockLLM, {
118
+ model: "gpt-4o-mini",
119
+ middlewares: [middleware]
120
+ });
121
+
122
+ // Middlewares are passed when ask() is called
123
+ await chat.ask("Hello");
124
+
125
+ // Verify chat was created with middlewares
126
+ expect(mockLLM.chat).toHaveBeenCalledWith(
127
+ "gpt-4o-mini",
128
+ expect.objectContaining({
129
+ middlewares: [middleware]
130
+ })
131
+ );
132
+
133
+ const captured = mockLLM.getMiddlewares();
134
+ expect(captured).toHaveLength(1);
135
+ expect(captured[0].name).toBe("TestMiddleware");
136
+ });
137
+ });