@ekodb/ekodb-client 0.13.0 → 0.15.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.
package/dist/client.js CHANGED
@@ -71,6 +71,7 @@ var MergeStrategy;
71
71
  class EkoDBClient {
72
72
  constructor(config, apiKey) {
73
73
  this.token = null;
74
+ this.tokenExpiry = 0;
74
75
  this.rateLimitInfo = null;
75
76
  // Support both old (baseURL, apiKey) and new (config object) signatures
76
77
  if (typeof config === "string") {
@@ -133,20 +134,68 @@ class EkoDBClient {
133
134
  }
134
135
  const result = (await response.json());
135
136
  this.token = result.token;
137
+ // Extract and cache JWT expiry for proactive refresh
138
+ const expiry = this.extractJWTExpiry(result.token);
139
+ this.tokenExpiry = expiry ?? Math.floor(Date.now() / 1000) + 3600; // fallback: 1 hour
136
140
  }
137
141
  /**
138
- * Get the current authentication token.
139
- * Returns null if not yet authenticated. Call refreshToken() first.
140
- */
141
- getToken() {
142
+ * Get a valid authentication token.
143
+ *
144
+ * Returns a cached token if it has more than 60s of validity remaining.
145
+ * Otherwise fetches a new one via refreshToken(). This means callers
146
+ * never need to handle token refresh themselves — every getToken() call
147
+ * returns a token that's valid for at least 60 more seconds.
148
+ */
149
+ async getToken() {
150
+ if (this.token) {
151
+ const now = Math.floor(Date.now() / 1000);
152
+ if (now + 60 >= this.tokenExpiry) {
153
+ // Token is about to expire or already expired — refresh proactively
154
+ await this.refreshToken();
155
+ }
156
+ }
157
+ else {
158
+ // No token yet — fetch one
159
+ await this.refreshToken();
160
+ }
142
161
  return this.token;
143
162
  }
144
163
  /**
145
- * Clear the cached authentication token.
164
+ * Clear the cached authentication token and expiry.
146
165
  * The next request will trigger a fresh token exchange.
147
166
  */
148
167
  clearTokenCache() {
149
168
  this.token = null;
169
+ this.tokenExpiry = 0;
170
+ }
171
+ /**
172
+ * Extract the `exp` claim from a JWT without verifying the signature.
173
+ * Returns the Unix timestamp (seconds) of expiry, or null if parsing fails.
174
+ */
175
+ extractJWTExpiry(token) {
176
+ try {
177
+ const parts = token.split(".");
178
+ if (parts.length !== 3) {
179
+ return null;
180
+ }
181
+ // Convert base64url to standard base64
182
+ let payload = parts[1];
183
+ payload = payload.replace(/-/g, "+").replace(/_/g, "/");
184
+ // Pad to multiple of 4
185
+ const pad = payload.length % 4;
186
+ if (pad) {
187
+ payload += "=".repeat(4 - pad);
188
+ }
189
+ const decoded = atob(payload);
190
+ const claims = JSON.parse(decoded);
191
+ if (typeof claims.exp === "number") {
192
+ return claims.exp;
193
+ }
194
+ return null;
195
+ }
196
+ catch {
197
+ return null;
198
+ }
150
199
  }
151
200
  /**
152
201
  * Extract rate limit information from response headers
@@ -881,6 +930,43 @@ class EkoDBClient {
881
930
  }
882
931
  }
883
932
  // ========== Chat Methods ==========
933
+ /**
934
+ * Execute a tool via ekoDB's server-side tool pipeline.
935
+ *
936
+ * Calls POST /api/chat/tools/execute which goes through the same
937
+ * execute_tool function as the LLM tool-calling loop — with all
938
+ * collection filtering, permission enforcement, and internal collection
939
+ * blocking. No LLM round-trip.
940
+ *
941
+ * @returns The tool result if executed, or null if the server doesn't
942
+ * support the endpoint (older ekoDB versions).
943
+ */
944
+ async executeTool(toolName, params, chatId) {
945
+ const body = { tool: toolName, params };
946
+ if (chatId) {
947
+ body.chat_id = chatId;
948
+ }
949
+ try {
950
+ const result = await this.makeRequest("POST", "/api/chat/tools/execute", body, 0, true);
951
+ if (result.success) {
952
+ return result.result;
953
+ }
954
+ else {
955
+ throw new Error(result.error || "tool execution failed");
956
+ }
957
+ }
958
+ catch (err) {
959
+ // Server doesn't have the endpoint (404) or route mismatch (405)
960
+ // Parse status from makeRequest error format: "Request failed with status NNN: ..."
961
+ const message = String(err?.message ?? "");
962
+ const match = message.match(/Request failed with status (\d+):/);
963
+ const status = match ? parseInt(match[1], 10) : undefined;
964
+ if (status === 404 || status === 405) {
965
+ return null;
966
+ }
967
+ throw err;
968
+ }
969
+ }
884
970
  /**
885
971
  * Create a new chat session
886
972
  */
@@ -906,12 +992,198 @@ class EkoDBClient {
906
992
  async rawCompletion(request) {
907
993
  return this.makeRequest("POST", "/api/chat/complete", request, 0, true);
908
994
  }
995
+ /**
996
+ * Stateless raw LLM completion via SSE streaming.
997
+ *
998
+ * Same as rawCompletion() but uses Server-Sent Events to keep the
999
+ * connection alive. Preferred for deployed instances where reverse proxies
1000
+ * may kill idle HTTP connections before the LLM responds.
1001
+ */
1002
+ async rawCompletionStream(request) {
1003
+ let token = await this.getToken();
1004
+ const url = `${this.baseURL}/api/chat/complete/stream`;
1005
+ const response = await fetch(url, {
1006
+ method: "POST",
1007
+ headers: {
1008
+ "Content-Type": "application/json",
1009
+ Accept: "text/event-stream",
1010
+ Authorization: `Bearer ${token}`,
1011
+ },
1012
+ body: JSON.stringify(request),
1013
+ });
1014
+ if (!response.ok) {
1015
+ const body = await response.text();
1016
+ throw new Error(`SSE raw completion failed (${response.status}): ${body}`);
1017
+ }
1018
+ const body = await response.text();
1019
+ let content = "";
1020
+ let lastError = null;
1021
+ for (const line of body.split("\n")) {
1022
+ if (line.startsWith("data:")) {
1023
+ const dataStr = line.slice(5).trim();
1024
+ if (!dataStr)
1025
+ continue;
1026
+ try {
1027
+ const eventData = JSON.parse(dataStr);
1028
+ if (eventData.token)
1029
+ content += eventData.token;
1030
+ if (eventData.content)
1031
+ content = eventData.content;
1032
+ if (eventData.error)
1033
+ lastError = eventData.error;
1034
+ }
1035
+ catch {
1036
+ // skip malformed SSE data
1037
+ }
1038
+ }
1039
+ }
1040
+ if (lastError)
1041
+ throw new Error(lastError);
1042
+ return { content };
1043
+ }
1044
+ /**
1045
+ * Stateless raw LLM completion via SSE streaming with token-level progress.
1046
+ *
1047
+ * Same as rawCompletionStream() but invokes `onToken` with each token as it
1048
+ * arrives, allowing callers to show real-time progress.
1049
+ */
1050
+ async rawCompletionStreamWithProgress(request, onToken) {
1051
+ let token = await this.getToken();
1052
+ const url = `${this.baseURL}/api/chat/complete/stream`;
1053
+ const response = await fetch(url, {
1054
+ method: "POST",
1055
+ headers: {
1056
+ "Content-Type": "application/json",
1057
+ Accept: "text/event-stream",
1058
+ Authorization: `Bearer ${token}`,
1059
+ },
1060
+ body: JSON.stringify(request),
1061
+ });
1062
+ if (!response.ok) {
1063
+ const body = await response.text();
1064
+ throw new Error(`SSE raw completion failed (${response.status}): ${body}`);
1065
+ }
1066
+ const body = await response.text();
1067
+ let content = "";
1068
+ let lastError = null;
1069
+ for (const line of body.split("\n")) {
1070
+ if (line.startsWith("data:")) {
1071
+ const dataStr = line.slice(5).trim();
1072
+ if (!dataStr)
1073
+ continue;
1074
+ try {
1075
+ const eventData = JSON.parse(dataStr);
1076
+ if (eventData.token) {
1077
+ content += eventData.token;
1078
+ onToken(eventData.token);
1079
+ }
1080
+ if (eventData.content)
1081
+ content = eventData.content;
1082
+ if (eventData.error)
1083
+ lastError = eventData.error;
1084
+ }
1085
+ catch {
1086
+ // skip malformed SSE data
1087
+ }
1088
+ }
1089
+ }
1090
+ if (lastError)
1091
+ throw new Error(lastError);
1092
+ return { content };
1093
+ }
909
1094
  /**
910
1095
  * Send a message in an existing chat session
911
1096
  */
912
1097
  async chatMessage(sessionId, request) {
913
1098
  return this.makeRequest("POST", `/api/chat/${sessionId}/messages`, request, 0, true);
914
1099
  }
1100
+ /**
1101
+ * Send a message in an existing chat session via SSE streaming.
1102
+ *
1103
+ * Returns an EventStream that emits ChatStreamEvent objects as they arrive:
1104
+ * - `{ type: "chunk", content: "..." }` for each token
1105
+ * - `{ type: "end", messageId, executionTimeMs, tokenUsage?, contextWindow? }` when complete
1106
+ * - `{ type: "error", error: "..." }` on failure
1107
+ *
1108
+ * Preferred over chatMessage() for long-running responses where reverse
1109
+ * proxies may kill idle HTTP connections before the LLM responds.
1110
+ */
1111
+ chatMessageStream(chatId, request) {
1112
+ const stream = new EventStream();
1113
+ (async () => {
1114
+ try {
1115
+ let token = this.getToken();
1116
+ if (!token) {
1117
+ await this.refreshToken();
1118
+ token = this.getToken();
1119
+ }
1120
+ const url = `${this.baseURL}/api/chat/${chatId}/messages/stream`;
1121
+ const response = await fetch(url, {
1122
+ method: "POST",
1123
+ headers: {
1124
+ "Content-Type": "application/json",
1125
+ Accept: "text/event-stream",
1126
+ Authorization: `Bearer ${token}`,
1127
+ },
1128
+ body: JSON.stringify(request),
1129
+ });
1130
+ if (!response.ok) {
1131
+ const body = await response.text();
1132
+ stream.emit("event", {
1133
+ type: "error",
1134
+ error: `SSE chat message stream failed (${response.status}): ${body}`,
1135
+ });
1136
+ stream.close();
1137
+ return;
1138
+ }
1139
+ const body = await response.text();
1140
+ for (const line of body.split("\n")) {
1141
+ if (!line.startsWith("data:"))
1142
+ continue;
1143
+ const dataStr = line.slice(5).trim();
1144
+ if (!dataStr)
1145
+ continue;
1146
+ try {
1147
+ const eventData = JSON.parse(dataStr);
1148
+ if (eventData.error) {
1149
+ stream.emit("event", {
1150
+ type: "error",
1151
+ error: eventData.error,
1152
+ });
1153
+ }
1154
+ else if (eventData.content && eventData.message_id) {
1155
+ // Done event — has full content + message_id
1156
+ stream.emit("event", {
1157
+ type: "end",
1158
+ messageId: eventData.message_id,
1159
+ executionTimeMs: eventData.execution_time_ms ?? 0,
1160
+ tokenUsage: eventData.token_usage,
1161
+ contextWindow: eventData.context_window,
1162
+ });
1163
+ }
1164
+ else if (eventData.token) {
1165
+ stream.emit("event", {
1166
+ type: "chunk",
1167
+ content: eventData.token,
1168
+ });
1169
+ }
1170
+ }
1171
+ catch {
1172
+ // skip malformed SSE data
1173
+ }
1174
+ }
1175
+ stream.close();
1176
+ }
1177
+ catch (err) {
1178
+ stream.emit("event", {
1179
+ type: "error",
1180
+ error: err.message ?? String(err),
1181
+ });
1182
+ stream.close();
1183
+ }
1184
+ })();
1185
+ return stream;
1186
+ }
915
1187
  /**
916
1188
  * Get a chat session by ID
917
1189
  */
@@ -1116,6 +1388,204 @@ class EkoDBClient {
1116
1388
  await this.makeRequest("DELETE", `/api/functions/${encodeURIComponent(label)}`, undefined, 0, true);
1117
1389
  }
1118
1390
  // ========================================================================
1391
+ // GOAL API
1392
+ // ========================================================================
1393
+ /** Create a new goal */
1394
+ async goalCreate(data) {
1395
+ return this.makeRequest("POST", "/api/chat/goals", data, 0, true);
1396
+ }
1397
+ /** List all goals */
1398
+ async goalList() {
1399
+ return this.makeRequest("GET", "/api/chat/goals", undefined, 0, true);
1400
+ }
1401
+ /** Get a goal by ID */
1402
+ async goalGet(id) {
1403
+ return this.makeRequest("GET", `/api/chat/goals/${encodeURIComponent(id)}`, undefined, 0, true);
1404
+ }
1405
+ /** Update a goal by ID */
1406
+ async goalUpdate(id, data) {
1407
+ return this.makeRequest("PUT", `/api/chat/goals/${encodeURIComponent(id)}`, data, 0, true);
1408
+ }
1409
+ /** Delete a goal by ID */
1410
+ async goalDelete(id) {
1411
+ await this.makeRequest("DELETE", `/api/chat/goals/${encodeURIComponent(id)}`, undefined, 0, true);
1412
+ }
1413
+ // ── Goal Templates ──
1414
+ /** Create a new goal template */
1415
+ async goalTemplateCreate(data) {
1416
+ return this.makeRequest("POST", "/api/chat/goal-templates", data, 0, true);
1417
+ }
1418
+ /** List all goal templates */
1419
+ async goalTemplateList() {
1420
+ return this.makeRequest("GET", "/api/chat/goal-templates", undefined, 0, true);
1421
+ }
1422
+ /** Get a goal template by ID */
1423
+ async goalTemplateGet(id) {
1424
+ return this.makeRequest("GET", `/api/chat/goal-templates/${encodeURIComponent(id)}`, undefined, 0, true);
1425
+ }
1426
+ /** Update a goal template by ID */
1427
+ async goalTemplateUpdate(id, data) {
1428
+ return this.makeRequest("PUT", `/api/chat/goal-templates/${encodeURIComponent(id)}`, data, 0, true);
1429
+ }
1430
+ /** Delete a goal template by ID */
1431
+ async goalTemplateDelete(id) {
1432
+ await this.makeRequest("DELETE", `/api/chat/goal-templates/${encodeURIComponent(id)}`, undefined, 0, true);
1433
+ }
1434
+ /** Search goals */
1435
+ async goalSearch(query) {
1436
+ const params = new URLSearchParams({ q: query });
1437
+ return this.makeRequest("GET", `/api/chat/goals/search?${params}`, undefined, 0, true);
1438
+ }
1439
+ /** Mark a goal as complete (status -> pending_review) */
1440
+ async goalComplete(id, data) {
1441
+ return this.makeRequest("POST", `/api/chat/goals/${encodeURIComponent(id)}/complete`, data, 0, true);
1442
+ }
1443
+ /** Approve a goal (status -> in_progress) */
1444
+ async goalApprove(id) {
1445
+ return this.makeRequest("POST", `/api/chat/goals/${encodeURIComponent(id)}/approve`, undefined, 0, true);
1446
+ }
1447
+ /** Reject a goal (status -> failed) */
1448
+ async goalReject(id, data) {
1449
+ return this.makeRequest("POST", `/api/chat/goals/${encodeURIComponent(id)}/reject`, data, 0, true);
1450
+ }
1451
+ /** Start a goal step (status -> in_progress) */
1452
+ async goalStepStart(id, stepIndex) {
1453
+ return this.makeRequest("POST", `/api/chat/goals/${encodeURIComponent(id)}/steps/${stepIndex}/start`, undefined, 0, true);
1454
+ }
1455
+ /** Complete a goal step with result */
1456
+ async goalStepComplete(id, stepIndex, data) {
1457
+ return this.makeRequest("POST", `/api/chat/goals/${encodeURIComponent(id)}/steps/${stepIndex}/complete`, data, 0, true);
1458
+ }
1459
+ /** Fail a goal step with error */
1460
+ async goalStepFail(id, stepIndex, data) {
1461
+ return this.makeRequest("POST", `/api/chat/goals/${encodeURIComponent(id)}/steps/${stepIndex}/fail`, data, 0, true);
1462
+ }
1463
+ // ========================================================================
1464
+ // TASK API
1465
+ // ========================================================================
1466
+ /** Create a new scheduled task */
1467
+ async taskCreate(data) {
1468
+ return this.makeRequest("POST", "/api/chat/tasks", data, 0, true);
1469
+ }
1470
+ /** List all scheduled tasks */
1471
+ async taskList() {
1472
+ return this.makeRequest("GET", "/api/chat/tasks", undefined, 0, true);
1473
+ }
1474
+ /** Get a task by ID */
1475
+ async taskGet(id) {
1476
+ return this.makeRequest("GET", `/api/chat/tasks/${encodeURIComponent(id)}`, undefined, 0, true);
1477
+ }
1478
+ /** Update a task by ID */
1479
+ async taskUpdate(id, data) {
1480
+ return this.makeRequest("PUT", `/api/chat/tasks/${encodeURIComponent(id)}`, data, 0, true);
1481
+ }
1482
+ /** Delete a task by ID */
1483
+ async taskDelete(id) {
1484
+ await this.makeRequest("DELETE", `/api/chat/tasks/${encodeURIComponent(id)}`, undefined, 0, true);
1485
+ }
1486
+ /** Get tasks that are due at the given time */
1487
+ async taskDue(now) {
1488
+ const params = new URLSearchParams({ now });
1489
+ return this.makeRequest("GET", `/api/chat/tasks/due?${params}`, undefined, 0, true);
1490
+ }
1491
+ /** Start a task (status -> running) */
1492
+ async taskStart(id) {
1493
+ return this.makeRequest("POST", `/api/chat/tasks/${encodeURIComponent(id)}/start`, undefined, 0, true);
1494
+ }
1495
+ /** Mark a task as succeeded */
1496
+ async taskSucceed(id, data) {
1497
+ return this.makeRequest("POST", `/api/chat/tasks/${encodeURIComponent(id)}/succeed`, data, 0, true);
1498
+ }
1499
+ /** Mark a task as failed */
1500
+ async taskFail(id, data) {
1501
+ return this.makeRequest("POST", `/api/chat/tasks/${encodeURIComponent(id)}/fail`, data, 0, true);
1502
+ }
1503
+ /** Pause a task */
1504
+ async taskPause(id) {
1505
+ return this.makeRequest("POST", `/api/chat/tasks/${encodeURIComponent(id)}/pause`, undefined, 0, true);
1506
+ }
1507
+ /** Resume a paused task */
1508
+ async taskResume(id, data) {
1509
+ return this.makeRequest("POST", `/api/chat/tasks/${encodeURIComponent(id)}/resume`, data, 0, true);
1510
+ }
1511
+ // ========================================================================
1512
+ // AGENT API
1513
+ // ========================================================================
1514
+ /** Create a new agent */
1515
+ async agentCreate(data) {
1516
+ return this.makeRequest("POST", "/api/chat/agents", data, 0, true);
1517
+ }
1518
+ /** List all agents */
1519
+ async agentList() {
1520
+ return this.makeRequest("GET", "/api/chat/agents", undefined, 0, true);
1521
+ }
1522
+ /** Get an agent by ID */
1523
+ async agentGet(id) {
1524
+ return this.makeRequest("GET", `/api/chat/agents/${encodeURIComponent(id)}`, undefined, 0, true);
1525
+ }
1526
+ /** Get an agent by name */
1527
+ async agentGetByName(name) {
1528
+ return this.makeRequest("GET", `/api/chat/agents/by-name/${encodeURIComponent(name)}`, undefined, 0, true);
1529
+ }
1530
+ /** Update an agent by ID */
1531
+ async agentUpdate(id, data) {
1532
+ return this.makeRequest("PUT", `/api/chat/agents/${encodeURIComponent(id)}`, data, 0, true);
1533
+ }
1534
+ /** Delete an agent by ID */
1535
+ async agentDelete(id) {
1536
+ await this.makeRequest("DELETE", `/api/chat/agents/${encodeURIComponent(id)}`, undefined, 0, true);
1537
+ }
1538
+ /** Get agents by deployment ID */
1539
+ async agentsByDeployment(deploymentId) {
1540
+ return this.makeRequest("GET", `/api/chat/agents/by-deployment/${encodeURIComponent(deploymentId)}`, undefined, 0, true);
1541
+ }
1542
+ // ========================================================================
1543
+ // KV DOCUMENT LINKING
1544
+ // ========================================================================
1545
+ /** Get documents linked to a KV key */
1546
+ async kvGetLinks(key) {
1547
+ return this.makeRequest("GET", `/api/kv/links/${encodeURIComponent(key)}`, undefined, 0, true);
1548
+ }
1549
+ /** Link a document to a KV key */
1550
+ async kvLink(key, collection, documentId) {
1551
+ return this.makeRequest("POST", `/api/kv/link`, { key, collection, document_id: documentId }, 0, true);
1552
+ }
1553
+ /** Unlink a document from a KV key */
1554
+ async kvUnlink(key, collection, documentId) {
1555
+ return this.makeRequest("POST", `/api/kv/unlink`, { key, collection, document_id: documentId }, 0, true);
1556
+ }
1557
+ // ========================================================================
1558
+ // SCHEDULE MANAGEMENT
1559
+ // ========================================================================
1560
+ /** Create a new schedule */
1561
+ async createSchedule(data) {
1562
+ return this.makeRequest("POST", `/api/schedules`, data, 0, true);
1563
+ }
1564
+ /** List all schedules */
1565
+ async listSchedules() {
1566
+ return this.makeRequest("GET", `/api/schedules`, undefined, 0, true);
1567
+ }
1568
+ /** Get a schedule by ID */
1569
+ async getSchedule(id) {
1570
+ return this.makeRequest("GET", `/api/schedules/${encodeURIComponent(id)}`, undefined, 0, true);
1571
+ }
1572
+ /** Update a schedule */
1573
+ async updateSchedule(id, data) {
1574
+ return this.makeRequest("PUT", `/api/schedules/${encodeURIComponent(id)}`, data, 0, true);
1575
+ }
1576
+ /** Delete a schedule */
1577
+ async deleteSchedule(id) {
1578
+ await this.makeRequest("DELETE", `/api/schedules/${encodeURIComponent(id)}`, undefined, 0, true);
1579
+ }
1580
+ /** Pause a schedule */
1581
+ async pauseSchedule(id) {
1582
+ return this.makeRequest("POST", `/api/schedules/${encodeURIComponent(id)}/pause`, undefined, 0, true);
1583
+ }
1584
+ /** Resume a schedule */
1585
+ async resumeSchedule(id) {
1586
+ return this.makeRequest("POST", `/api/schedules/${encodeURIComponent(id)}/resume`, undefined, 0, true);
1587
+ }
1588
+ // ========================================================================
1119
1589
  // COLLECTION UTILITIES
1120
1590
  // ========================================================================
1121
1591
  /**
@@ -1385,7 +1855,12 @@ class WebSocketClient {
1385
1855
  switch (msg.type) {
1386
1856
  case "Success":
1387
1857
  case "Error": {
1388
- const messageId = msg.payload?.message_id || msg.payload?.messageId;
1858
+ // Try messageId from top-level, then from payload
1859
+ const messageId = msg.messageId ||
1860
+ msg.message_id ||
1861
+ msg.payload?.message_id ||
1862
+ msg.payload?.messageId;
1863
+ let matched = false;
1389
1864
  if (messageId && this.pendingRequests.has(messageId)) {
1390
1865
  const pending = this.pendingRequests.get(messageId);
1391
1866
  this.pendingRequests.delete(messageId);
@@ -1395,8 +1870,9 @@ class WebSocketClient {
1395
1870
  else {
1396
1871
  pending.resolve(msg.payload);
1397
1872
  }
1873
+ matched = true;
1398
1874
  }
1399
- else if (this.registerToolsAck) {
1875
+ if (!matched && this.registerToolsAck) {
1400
1876
  const ack = this.registerToolsAck;
1401
1877
  this.registerToolsAck = null;
1402
1878
  if (msg.type === "Error") {
@@ -1405,6 +1881,21 @@ class WebSocketClient {
1405
1881
  else {
1406
1882
  ack.resolve(msg.payload);
1407
1883
  }
1884
+ matched = true;
1885
+ }
1886
+ // Server doesn't echo messageId — if there's exactly one pending
1887
+ // request, deliver the response to it (sequential request/response).
1888
+ if (!matched && this.pendingRequests.size === 1) {
1889
+ const entry = this.pendingRequests.entries().next().value;
1890
+ const key = entry[0];
1891
+ const pending = entry[1];
1892
+ this.pendingRequests.delete(key);
1893
+ if (msg.type === "Error") {
1894
+ pending.reject(new Error(msg.message || "Unknown error"));
1895
+ }
1896
+ else {
1897
+ pending.resolve(msg.payload);
1898
+ }
1408
1899
  }
1409
1900
  break;
1410
1901
  }
@@ -1445,6 +1936,7 @@ class WebSocketClient {
1445
1936
  tokenUsage: msg.payload.token_usage || msg.payload.tokenUsage,
1446
1937
  toolCallHistory: msg.payload.tool_call_history || msg.payload.toolCallHistory,
1447
1938
  executionTimeMs: msg.payload.execution_time_ms || msg.payload.executionTimeMs || 0,
1939
+ contextWindow: msg.payload.context_window || msg.payload.contextWindow,
1448
1940
  });
1449
1941
  this.chatStreams.delete(chatId);
1450
1942
  stream.close();
@@ -1604,6 +2096,30 @@ class WebSocketClient {
1604
2096
  };
1605
2097
  this.ws.send(JSON.stringify(request));
1606
2098
  }
2099
+ /**
2100
+ * Stateless raw LLM completion via WebSocket.
2101
+ *
2102
+ * Sends a RawComplete message and waits for the Success response.
2103
+ * Preferred over HTTP for deployed instances: the persistent WSS
2104
+ * connection is already authenticated and won't be killed by reverse
2105
+ * proxy timeouts.
2106
+ */
2107
+ async rawCompletion(request) {
2108
+ await this.ensureConnected();
2109
+ const messageId = this.genMessageId();
2110
+ const payload = await this.sendRequest({
2111
+ type: "RawComplete",
2112
+ messageId,
2113
+ payload: {
2114
+ system_prompt: request.system_prompt,
2115
+ message: request.message,
2116
+ ...(request.provider && { provider: request.provider }),
2117
+ ...(request.model && { model: request.model }),
2118
+ ...(request.max_tokens != null && { max_tokens: request.max_tokens }),
2119
+ },
2120
+ });
2121
+ return { content: payload?.data?.content || "" };
2122
+ }
1607
2123
  /**
1608
2124
  * Close the WebSocket connection.
1609
2125
  */