@xalia/agent 0.5.7 → 0.5.8

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.
Files changed (35) hide show
  1. package/dist/agent/src/agent/agent.js +3 -0
  2. package/dist/agent/src/agent/agentUtils.js +6 -12
  3. package/dist/agent/src/agent/mcpServerManager.js +1 -1
  4. package/dist/agent/src/agent/openAILLMStreaming.js +14 -7
  5. package/dist/agent/src/agent/sudoMcpServerManager.js +13 -13
  6. package/dist/agent/src/chat/client.js +24 -63
  7. package/dist/agent/src/chat/conversationManager.js +122 -12
  8. package/dist/agent/src/chat/db.js +9 -0
  9. package/dist/agent/src/chat/messages.js +27 -0
  10. package/dist/agent/src/chat/websocket.js +14 -0
  11. package/dist/agent/src/test/db.test.js +16 -0
  12. package/dist/agent/src/test/mcpServerManager.test.js +2 -2
  13. package/dist/agent/src/test/openaiStreaming.test.js +56 -28
  14. package/dist/agent/src/test/sudoMcpServerManager.test.js +10 -13
  15. package/dist/agent/src/tool/chatMain.js +7 -1
  16. package/dist/agent/src/tool/commandPrompt.js +9 -10
  17. package/package.json +1 -1
  18. package/src/agent/agent.ts +14 -4
  19. package/src/agent/agentUtils.ts +17 -23
  20. package/src/agent/mcpServerManager.ts +3 -1
  21. package/src/agent/openAILLMStreaming.ts +14 -7
  22. package/src/agent/sudoMcpServerManager.ts +17 -18
  23. package/src/chat/client.ts +35 -45
  24. package/src/chat/conversationManager.ts +147 -12
  25. package/src/chat/db.ts +14 -0
  26. package/src/chat/messages.ts +31 -0
  27. package/src/chat/websocket.ts +14 -0
  28. package/src/test/db.test.ts +22 -1
  29. package/src/test/mcpServerManager.test.ts +2 -2
  30. package/src/test/openaiStreaming.test.ts +64 -30
  31. package/src/test/sudoMcpServerManager.test.ts +11 -16
  32. package/src/tool/chatMain.ts +11 -2
  33. package/src/tool/commandPrompt.ts +8 -15
  34. package/dist/agent/src/chat/frontendClient.js +0 -74
  35. package/src/chat/frontendClient.ts +0 -123
@@ -1,20 +1,16 @@
1
1
  import { Tool } from "@modelcontextprotocol/sdk/types.js";
2
2
  import { strict as assert } from "assert";
3
- import * as websocket from "ws";
4
3
 
5
4
  import { AgentProfile, McpServerBrief, getLogger } from "@xalia/xmcp/sdk";
6
5
 
7
6
  import { ISkillManager } from "../agent/sudoMcpServerManager";
8
- import {
9
- IMcpServerManager,
10
- McpServerInfo,
11
- McpServerInfoRW,
12
- } from "../agent/mcpServerManager";
13
-
14
- import { ServerToClient, ClientToServer } from "./messages";
7
+ import { McpServerInfo, McpServerInfoRW } from "../agent/mcpServerManager";
15
8
  import { ChatCompletionMessageParam, IConversation } from "../agent/agent";
16
9
  import { IPlatform } from "../agent/iplatform";
17
10
 
11
+ import { WebSocket } from "./websocket";
12
+ import { ServerToClient, ClientToServer } from "./messages";
13
+
18
14
  const logger = getLogger();
19
15
 
20
16
  type OnMessageCallback = (msg: ServerToClient) => void;
@@ -25,10 +21,19 @@ interface IMessageSender {
25
21
  sendMessage(message: ClientToServer): void;
26
22
  }
27
23
 
28
- class RemoteMcpServerManager implements IMcpServerManager {
24
+ class RemoteSudoMcpServerManager implements ISkillManager {
25
+ private sender: IMessageSender;
26
+ private briefs: McpServerBrief[];
27
+ private briefsMap: Record<string, McpServerBrief>;
29
28
  private mcpServers: { [serverName: string]: McpServerInfoRW } = {};
30
29
 
31
- constructor(private sender: IMessageSender) {
30
+ constructor(sender: IMessageSender, briefs: McpServerBrief[]) {
31
+ this.sender = sender;
32
+ this.briefs = briefs;
33
+ this.briefsMap = {};
34
+ briefs.forEach((b) => {
35
+ this.briefsMap[b.name] = b;
36
+ });
32
37
  this.mcpServers = {};
33
38
  }
34
39
 
@@ -161,25 +166,6 @@ class RemoteMcpServerManager implements IMcpServerManager {
161
166
 
162
167
  server.disableTool(toolName);
163
168
  }
164
- }
165
-
166
- class RemoteSudoMcpServerManager implements ISkillManager {
167
- private briefsMap: Record<string, McpServerBrief>;
168
-
169
- constructor(
170
- private sender: IMessageSender,
171
- private briefs: McpServerBrief[],
172
- private msm: RemoteMcpServerManager
173
- ) {
174
- this.briefsMap = {};
175
- briefs.forEach((b) => {
176
- this.briefsMap[b.name] = b;
177
- });
178
- }
179
-
180
- getMcpServerManager(): IMcpServerManager {
181
- return this.msm;
182
- }
183
169
 
184
170
  getServerBriefs(): McpServerBrief[] {
185
171
  return this.briefs;
@@ -196,22 +182,23 @@ class RemoteSudoMcpServerManager implements ISkillManager {
196
182
  enable_all,
197
183
  });
198
184
  }
185
+
186
+ async shutdown(): Promise<void> {}
199
187
  }
200
188
 
201
189
  export class ChatClient implements IMessageSender, IConversation {
202
190
  private platform: IPlatform;
203
- private ws: websocket.WebSocket;
191
+ private ws: WebSocket;
204
192
  private onMessageCB: OnMessageCallback;
205
193
  private onConnectionClosedCB: OnConnectionClosedCallback;
206
194
  private closed: boolean;
207
- private msm: RemoteMcpServerManager;
208
195
  private smsm: RemoteSudoMcpServerManager;
209
196
  private systemPrompt: string;
210
197
  private model: string;
211
198
 
212
199
  private constructor(
213
200
  platform: IPlatform,
214
- ws: websocket.WebSocket,
201
+ ws: WebSocket,
215
202
  onMessageCB: OnMessageCallback,
216
203
  onConnectionClosedCB: OnConnectionClosedCallback,
217
204
  serverBriefs: McpServerBrief[]
@@ -221,8 +208,7 @@ export class ChatClient implements IMessageSender, IConversation {
221
208
  this.onMessageCB = onMessageCB;
222
209
  this.onConnectionClosedCB = onConnectionClosedCB;
223
210
  this.closed = false;
224
- this.msm = new RemoteMcpServerManager(this);
225
- this.smsm = new RemoteSudoMcpServerManager(this, serverBriefs, this.msm);
211
+ this.smsm = new RemoteSudoMcpServerManager(this, serverBriefs);
226
212
  this.systemPrompt = "";
227
213
  this.model = "";
228
214
  }
@@ -240,7 +226,7 @@ export class ChatClient implements IMessageSender, IConversation {
240
226
  const urlParams = new URLSearchParams(params);
241
227
  const url = `ws://${host}:${port}?${urlParams}`;
242
228
 
243
- const ws = new websocket.WebSocket(url, [token]);
229
+ const ws = new WebSocket(url, [token]);
244
230
  logger.info("created ws");
245
231
 
246
232
  let client: ChatClient | undefined = undefined;
@@ -271,7 +257,7 @@ export class ChatClient implements IMessageSender, IConversation {
271
257
  ws.onopen = async () => {
272
258
  logger.info("opened");
273
259
 
274
- ws.onmessage = (ev: websocket.MessageEvent) => {
260
+ ws.onmessage = (ev: MessageEvent) => {
275
261
  try {
276
262
  const msgData = ev.data;
277
263
  if (typeof msgData !== "string") {
@@ -288,10 +274,10 @@ export class ChatClient implements IMessageSender, IConversation {
288
274
  };
289
275
  };
290
276
 
291
- ws.onclose = async (err) => {
277
+ ws.onclose = async (ev: CloseEvent) => {
292
278
  logger.info("closed");
293
279
  logger.info(
294
- `[client] WebSocket connection closed: ${JSON.stringify(err)}`
280
+ `[client] WebSocket connection closed: ${JSON.stringify(ev)}`
295
281
  );
296
282
  if (client) {
297
283
  client.closed = true;
@@ -299,9 +285,9 @@ export class ChatClient implements IMessageSender, IConversation {
299
285
  await onConnectionClosedCB();
300
286
  };
301
287
 
302
- ws.onerror = async (error) => {
303
- logger.error("[client] WebSocket error:", JSON.stringify(error));
304
- e(error);
288
+ ws.onerror = async (ev: Event) => {
289
+ logger.error("[client] WebSocket error:", JSON.stringify(ev));
290
+ e(ev);
305
291
 
306
292
  if (client) {
307
293
  client.closed = true;
@@ -381,6 +367,10 @@ export class ChatClient implements IMessageSender, IConversation {
381
367
  throw "resetConversation not implemented for ChatClient";
382
368
  }
383
369
 
370
+ shutdown(): Promise<void> {
371
+ throw "shutdown not implemented for ChatClient";
372
+ }
373
+
384
374
  public sendMessage(message: ClientToServer): undefined {
385
375
  assert(!this.closed);
386
376
  const data = JSON.stringify(message);
@@ -401,20 +391,20 @@ export class ChatClient implements IMessageSender, IConversation {
401
391
  //
402
392
 
403
393
  case "mcp_server_added":
404
- this.msm.onMcpServerAdded(
394
+ this.smsm.onMcpServerAdded(
405
395
  message.server_name,
406
396
  message.tools,
407
397
  message.enabled_tools
408
398
  );
409
399
  break;
410
400
  case "mcp_server_removed":
411
- this.msm.onMcpServerRemoved(message.server_name);
401
+ this.smsm.onMcpServerRemoved(message.server_name);
412
402
  break;
413
403
  case "mcp_server_tool_enabled":
414
- this.msm.onMcpServerToolEnabled(message.server_name, message.tool);
404
+ this.smsm.onMcpServerToolEnabled(message.server_name, message.tool);
415
405
  break;
416
406
  case "mcp_server_tool_disabled":
417
- this.msm.onMcpServerToolDisabled(message.server_name, message.tool);
407
+ this.smsm.onMcpServerToolDisabled(message.server_name, message.tool);
418
408
  break;
419
409
  case "system_prompt_updated":
420
410
  this.systemPrompt = message.system_prompt;
@@ -8,6 +8,8 @@ import {
8
8
  Agent,
9
9
  ChatCompletionMessageParam,
10
10
  ChatCompletionMessageToolCall,
11
+ ChatCompletionUserMessageParam,
12
+ createUserMessage,
11
13
  } from "../agent/agent";
12
14
  import { SkillManager } from "../agent/sudoMcpServerManager";
13
15
  import { createAgentWithSkills } from "../agent/agentUtils";
@@ -21,6 +23,7 @@ import type {
21
23
  ServerMcpServerToolDisabled,
22
24
  ServerSystemPromptUpdated,
23
25
  ServerModelUpdated,
26
+ ServerUserMessage,
24
27
  } from "./messages";
25
28
  import { AsyncQueue } from "./asyncQueue";
26
29
  import { Database, UserData } from "./db";
@@ -81,27 +84,49 @@ export class OpenSession {
81
84
  throw new UserAlreadyConnected();
82
85
  }
83
86
 
84
- // Inform any other participants, and add the WebSocket to the map.
87
+ // Inform any other participants, then add the new WebSocket to the map.
85
88
 
86
89
  this.broadcast({ type: "user_joined", user: userName });
87
90
  this.connections[userName] = ws;
88
91
 
89
- // TODO:
90
- //
91
- // - send conversation state (ServerHistory)
92
+ // Send MCP server briefs first (client expects this)
92
93
 
93
94
  const briefs = this.skillManager.getServerBriefs();
94
95
  this.sendTo(userName, { type: "mcp_server_briefs", server_briefs: briefs });
95
96
 
97
+ // Send conversation
98
+
99
+ const conversation = this.agent.getConversation();
100
+ const convMessages = conversationToChatMessages(conversation, userName);
101
+ for (const chatMsg of convMessages) {
102
+ this.sendTo(userName, chatMsg);
103
+ }
104
+
105
+ // Update the MSM state
106
+
107
+ const agentProfile = this.agent.getAgentProfile();
96
108
  const msm = this.agent.getMcpServerManager();
97
- const mcpServerNames = msm.getMcpServerNames();
98
- for (const serverName of mcpServerNames) {
99
- const info = msm.getMcpServer(serverName);
109
+ for (const server_name in agentProfile.mcp_settings) {
110
+ const tools = msm.getMcpServer(server_name).getTools();
111
+ const enabled_tools = agentProfile.mcp_settings[server_name];
100
112
  this.sendTo(userName, {
101
113
  type: "mcp_server_added",
102
- server_name: serverName,
103
- tools: info.getTools(),
104
- enabled_tools: Object.keys(info.getEnabledTools()),
114
+ server_name,
115
+ tools,
116
+ enabled_tools,
117
+ });
118
+ }
119
+
120
+ // Send system prompt and model
121
+
122
+ this.sendTo(userName, {
123
+ type: "system_prompt_updated",
124
+ system_prompt: agentProfile.system_prompt,
125
+ });
126
+ if (agentProfile.model) {
127
+ this.sendTo(userName, {
128
+ type: "model_updated",
129
+ model: agentProfile.model,
105
130
  });
106
131
  }
107
132
 
@@ -215,7 +240,11 @@ export class OpenSession {
215
240
  });
216
241
  }
217
242
 
218
- async onChatMessage(message: string, userToken: string): Promise<undefined> {
243
+ async onChatMessage(
244
+ message: string,
245
+ // imageB64: string | undefined,
246
+ userToken: string
247
+ ): Promise<undefined> {
219
248
  // We manually broadcast the user's message here and start the agent
220
249
  // conversation, and then wait to get back all data from the agent before
221
250
  // processing further messages from clients.
@@ -225,11 +254,33 @@ export class OpenSession {
225
254
  type: "user_msg",
226
255
  message_id: msgId,
227
256
  message,
257
+ // imageB64,
228
258
  from: userToken,
229
259
  });
230
260
 
231
261
  this.curAgentMsgId = `${msgId}-resp`;
232
- await this.agent.userMessageEx(message, undefined, userToken);
262
+ const userMessageParam = createUserMessage(
263
+ message,
264
+ /*imageB64*/ undefined,
265
+ userToken
266
+ );
267
+ if (!userMessageParam) {
268
+ logger.debug(`ignoring empty message: ${message}`);
269
+ return;
270
+ }
271
+
272
+ const assistantReply = await this.agent.userMessageRaw(userMessageParam);
273
+ if (assistantReply) {
274
+ // TODO: consider including all messages (including tool calls)
275
+ // const newEntries = agent.getTrailingEntries(prevConvLength);
276
+ // await this.db.sessionConversationAppend(this.sessionUUID, newEntries);
277
+
278
+ const newEntries: ChatCompletionMessageParam[] = [
279
+ userMessageParam,
280
+ assistantReply,
281
+ ];
282
+ await this.db.sessionConversationAppend(this.sessionUUID, newEntries);
283
+ }
233
284
  }
234
285
 
235
286
  async onAddMcpServer(
@@ -593,3 +644,87 @@ export class ConversationManager {
593
644
  return openSession;
594
645
  }
595
646
  }
647
+
648
+ export function userMessageToChatMessage(
649
+ userMessage: ChatCompletionUserMessageParam,
650
+ message_id: string,
651
+ defaultName: string
652
+ ): ServerUserMessage | undefined {
653
+ const from = userMessage.name || defaultName;
654
+ if (typeof userMessage.content === "string") {
655
+ return {
656
+ type: "user_msg",
657
+ message_id,
658
+ message: userMessage.content,
659
+ from,
660
+ };
661
+ }
662
+
663
+ let message = "";
664
+ let image = undefined;
665
+ for (const content of userMessage.content) {
666
+ switch (content.type) {
667
+ case "text":
668
+ message += content.text;
669
+ break;
670
+ case "image_url":
671
+ assert(!image, "only one image per message supported");
672
+ image = content.image_url;
673
+ break;
674
+ case "input_audio":
675
+ throw "userMessageToChatMessage: audio content not supported";
676
+ case "file":
677
+ throw "userMessageToChatMessage: file content not supported";
678
+ default:
679
+ throw "userMessageToChatMessage: unexpected content.type";
680
+ }
681
+ }
682
+
683
+ if (image) {
684
+ throw "unimpl: image content";
685
+ }
686
+
687
+ return {
688
+ type: "user_msg",
689
+ message_id,
690
+ message,
691
+ from,
692
+ };
693
+ }
694
+
695
+ export function conversationToChatMessages(
696
+ conversation: ChatCompletionMessageParam[],
697
+ defaultName: string
698
+ ): ServerToClient[] {
699
+ const msgs: ServerToClient[] = [];
700
+ for (const ccmp of conversation) {
701
+ const message_id = `message_${msgs.length}`;
702
+ switch (ccmp.role) {
703
+ case "developer":
704
+ throw "developer messages not handled yet";
705
+ case "assistant":
706
+ assert(!ccmp.audio);
707
+ if (ccmp.content) {
708
+ msgs.push({
709
+ type: "agent_msg",
710
+ message: ccmp,
711
+ message_id,
712
+ });
713
+ }
714
+ // TODO: do we want to convert tool calls etc?
715
+ break;
716
+ case "user":
717
+ {
718
+ const msg = userMessageToChatMessage(ccmp, message_id, defaultName);
719
+ if (msg) {
720
+ msgs.push(msg);
721
+ }
722
+ }
723
+ break;
724
+ default:
725
+ break;
726
+ }
727
+ }
728
+
729
+ return msgs;
730
+ }
package/src/chat/db.ts CHANGED
@@ -6,6 +6,7 @@ import {
6
6
  getLogger,
7
7
  SavedAgentProfile,
8
8
  } from "@xalia/xmcp/sdk";
9
+ import { ChatCompletionMessageParam } from "../agent/agent";
9
10
 
10
11
  const logger = getLogger();
11
12
 
@@ -284,6 +285,19 @@ export class Database {
284
285
  return data[0].uuid;
285
286
  }
286
287
 
288
+ async sessionConversationAppend(
289
+ session_id: string,
290
+ newEntries: ChatCompletionMessageParam[]
291
+ ): Promise<void> {
292
+ const { error } = await this.client.rpc("session_append_to_conversation", {
293
+ session_id,
294
+ messages: newEntries,
295
+ });
296
+ if (error) {
297
+ throw error;
298
+ }
299
+ }
300
+
287
301
  async clearSessions(): Promise<void> {
288
302
  await this.client.from("sessions").delete().neq("uuid", "");
289
303
  }
@@ -233,3 +233,34 @@ export type ServerToClient =
233
233
  | ServerTyping
234
234
  | ServerToClientStateUpdate
235
235
  | ServerToClientActions;
236
+
237
+ export function decodeAssistantMessageParam(
238
+ msg: ChatCompletionAssistantMessageParam
239
+ ): string {
240
+ let text = "";
241
+
242
+ if (msg.audio) {
243
+ throw "decodeAssistantMessageParam; audio unimplemented";
244
+ }
245
+
246
+ if (msg.content) {
247
+ if (typeof msg.content === "string") {
248
+ text = msg.content;
249
+ } else if (msg.content instanceof Array) {
250
+ for (const c of msg.content) {
251
+ switch (c.type) {
252
+ case "text":
253
+ text += c.text;
254
+ break;
255
+ case "refusal":
256
+ text += c.refusal;
257
+ break;
258
+ default:
259
+ throw Error("unexpected AssistantMessageParam.content entry");
260
+ }
261
+ }
262
+ }
263
+ }
264
+
265
+ return text;
266
+ }
@@ -0,0 +1,14 @@
1
+ // Browser and Node.js Universal WebSocket wrapper
2
+
3
+ let WSClass: typeof WebSocket;
4
+
5
+ if (typeof window !== "undefined" && typeof window.WebSocket !== "undefined") {
6
+ // Running in browser
7
+ WSClass = window.WebSocket;
8
+ } else {
9
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
10
+ const ws = require("ws");
11
+ WSClass = ws.WebSocket as unknown as typeof WebSocket;
12
+ }
13
+
14
+ export { WSClass as WebSocket };
@@ -1,6 +1,6 @@
1
1
  import { expect } from "chai";
2
2
  import { Database, SUPABASE_LOCAL_KEY, SUPABASE_LOCAL_URL } from "../chat/db";
3
- import { AgentProfile } from "../agent/agent";
3
+ import { AgentProfile, ChatCompletionMessageParam } from "../agent/agent";
4
4
  import { strict as assert } from "assert";
5
5
  import { ApiClient } from "@xalia/xmcp/sdk";
6
6
  import { LOCAL_SERVER_URL } from "../agent/sudoMcpServerManager";
@@ -108,6 +108,15 @@ describe("DB", () => {
108
108
  });
109
109
 
110
110
  it("should create and retrieve sessions", async function () {
111
+ const CONV_0: ChatCompletionMessageParam[] = [
112
+ { role: "user", content: "message 0" },
113
+ { role: "assistant", content: "message 1" },
114
+ ];
115
+ const CONV_1: ChatCompletionMessageParam[] = [
116
+ { role: "user", content: "message 2" },
117
+ { role: "assistant", content: "message 3" },
118
+ ];
119
+
111
120
  const db = getLocalDB();
112
121
 
113
122
  await db.clearAgentProfiles();
@@ -132,5 +141,17 @@ describe("DB", () => {
132
141
  expect(session.title).eql("test_session");
133
142
  expect(session.user_uuid).eql("dummy_user");
134
143
  expect(session.uuid).eql(sessionId);
144
+
145
+ // Append messages to empty conversation
146
+
147
+ await db.sessionConversationAppend(sessionId, CONV_0);
148
+ const session0 = (await db.getSessionById(sessionId))!;
149
+ expect(session0.conversation).eql(CONV_0);
150
+
151
+ // Append further messages
152
+
153
+ await db.sessionConversationAppend(sessionId, CONV_1);
154
+ const session1 = (await db.getSessionById(sessionId))!;
155
+ expect(session1.conversation).eql(CONV_0.concat(CONV_1));
135
156
  });
136
157
  });
@@ -28,7 +28,7 @@ async function shutdownAll() {
28
28
  describe("McpServerManager", async () => {
29
29
  it("should initialize with correct descriptions", async (): Promise<void> => {
30
30
  const tm = getMcpServerManager();
31
- await tm.addMcpServer(
31
+ await tm.addMcpServerWithSSEUrl(
32
32
  "simplecalc",
33
33
  "http://localhost:5001/mcpservers/sudomcp/simplecalc/session",
34
34
  "dummy_key"
@@ -79,7 +79,7 @@ describe("McpServerManager", async () => {
79
79
  it("add / remove servers", async () => {
80
80
  const tm = getMcpServerManager();
81
81
 
82
- await tm.addMcpServer(
82
+ await tm.addMcpServerWithSSEUrl(
83
83
  "simplecalc",
84
84
  "http://localhost:5001/mcpservers/sudomcp/simplecalc/session",
85
85
  "dummy_key"
@@ -6,7 +6,9 @@ import {
6
6
  updateCompletion,
7
7
  } from "../agent/openAILLMStreaming";
8
8
 
9
- const TEST_STANDARD: OpenAI.Chat.Completions.ChatCompletionChunk[] = [
9
+ type ChatCompletionChunk = OpenAI.Chat.Completions.ChatCompletionChunk;
10
+
11
+ const TEST_STANDARD: ChatCompletionChunk[] = [
10
12
  {
11
13
  id: "chatcmpl-BzBqzVs5w0kU5we3KIUN1Q4FAZXjg",
12
14
  choices: [
@@ -34,7 +36,7 @@ const TEST_STANDARD: OpenAI.Chat.Completions.ChatCompletionChunk[] = [
34
36
  },
35
37
  ];
36
38
 
37
- const TEST_TRAILING_USAGE: OpenAI.Chat.Completions.ChatCompletionChunk[] = [
39
+ const TEST_TRAILING_USAGE: ChatCompletionChunk[] = [
38
40
  {
39
41
  id: "gen-1753923406-nsIKHyFRoJqkUntBnQTw",
40
42
  choices: [
@@ -73,6 +75,55 @@ const TEST_TRAILING_USAGE: OpenAI.Chat.Completions.ChatCompletionChunk[] = [
73
75
  },
74
76
  ];
75
77
 
78
+ const AGGREGATED_MESSAGE: OpenAI.Chat.Completions.ChatCompletion = {
79
+ id: "gen-1753923406-nsIKHyFRoJqkUntBnQTw",
80
+ choices: [
81
+ {
82
+ message: {
83
+ content: "test",
84
+ role: "assistant",
85
+ refusal: null,
86
+ tool_calls: undefined,
87
+ },
88
+ finish_reason: "stop",
89
+ index: 0,
90
+ logprobs: null,
91
+ },
92
+ ],
93
+ created: 1753923406,
94
+ model: "openai/gpt-4o",
95
+ object: "chat.completion",
96
+ service_tier: undefined,
97
+ system_fingerprint: "fp_a288987b44",
98
+ usage: {
99
+ completion_tokens: 50,
100
+ prompt_tokens: 271,
101
+ total_tokens: 321,
102
+ completion_tokens_details: { reasoning_tokens: 0 },
103
+ prompt_tokens_details: { cached_tokens: 0 },
104
+ },
105
+ };
106
+
107
+ const TEST_TRAILING_USAGE_WITH_3_CHUNKS: ChatCompletionChunk[] = [
108
+ {
109
+ ...TEST_TRAILING_USAGE[0],
110
+ choices: [
111
+ {
112
+ delta: { content: "test", role: "assistant" },
113
+ finish_reason: null,
114
+ index: 0,
115
+ logprobs: null,
116
+ },
117
+ ],
118
+ },
119
+ {
120
+ ...TEST_TRAILING_USAGE[1],
121
+ choices: [{ ...TEST_TRAILING_USAGE[1].choices[0], finish_reason: "stop" }],
122
+ usage: undefined,
123
+ },
124
+ { ...TEST_TRAILING_USAGE[1], choices: [] },
125
+ ];
126
+
76
127
  describe("OpenAI Streaming", () => {
77
128
  it("should support standard termination", async function () {
78
129
  const chunks = TEST_STANDARD;
@@ -110,33 +161,16 @@ describe("OpenAI Streaming", () => {
110
161
  const { initMessage } = initializeCompletion(chunks[0]);
111
162
  updateCompletion(initMessage, chunks[1]);
112
163
 
113
- expect(initMessage).eql({
114
- id: "gen-1753923406-nsIKHyFRoJqkUntBnQTw",
115
- choices: [
116
- {
117
- message: {
118
- content: "test",
119
- role: "assistant",
120
- refusal: null,
121
- tool_calls: undefined,
122
- },
123
- finish_reason: "stop",
124
- index: 0,
125
- logprobs: null,
126
- },
127
- ],
128
- created: 1753923406,
129
- model: "openai/gpt-4o",
130
- object: "chat.completion",
131
- service_tier: undefined,
132
- system_fingerprint: "fp_a288987b44",
133
- usage: {
134
- completion_tokens: 50,
135
- prompt_tokens: 271,
136
- total_tokens: 321,
137
- completion_tokens_details: { reasoning_tokens: 0 },
138
- prompt_tokens_details: { cached_tokens: 0 },
139
- },
140
- });
164
+ expect(initMessage).eql(AGGREGATED_MESSAGE);
165
+ });
166
+
167
+ it("should support trailing usage with 3 chunks", async function () {
168
+ const chunks = TEST_TRAILING_USAGE_WITH_3_CHUNKS;
169
+ expect(chunks.length).eql(3);
170
+ const { initMessage } = initializeCompletion(chunks[0]);
171
+ updateCompletion(initMessage, chunks[1]);
172
+ updateCompletion(initMessage, chunks[2]);
173
+
174
+ expect(initMessage).eql(AGGREGATED_MESSAGE);
141
175
  });
142
176
  });