@ekodb/ekodb-client 0.24.0 → 0.26.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/README.md +5 -2
- package/dist/client.d.ts +109 -2
- package/dist/client.js +160 -47
- package/dist/client.test.js +264 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.js +6 -1
- package/dist/websocket.test.js +66 -0
- package/package.json +2 -2
- package/src/client.test.ts +321 -0
- package/src/client.ts +254 -13
- package/src/index.ts +8 -0
- package/src/websocket.test.ts +90 -0
- package/tsconfig.json +2 -2
package/README.md
CHANGED
|
@@ -308,8 +308,11 @@ const joinResults = await client.find("users", multiQuery);
|
|
|
308
308
|
|
|
309
309
|
#### Chat Models
|
|
310
310
|
|
|
311
|
-
- `getChatModels(): Promise<
|
|
312
|
-
|
|
311
|
+
- `getChatModels(): Promise<ChatModels>` - Get all available chat models by
|
|
312
|
+
provider (`openai`, `anthropic`, `perplexity`, `gemini`), plus a per-provider
|
|
313
|
+
`providers` status map (`ok`, `not_configured`, `auth_failed`,
|
|
314
|
+
`permission_denied`, `billing`, `rate_limited`, `unavailable`, `unreachable`,
|
|
315
|
+
`request_error`) so a rejected key is distinguishable from a missing one
|
|
313
316
|
- `getChatModel(provider: string): Promise<string[]>` - Get models for a
|
|
314
317
|
specific provider
|
|
315
318
|
|
package/dist/client.d.ts
CHANGED
|
@@ -94,6 +94,55 @@ export interface UpsertOptions {
|
|
|
94
94
|
transactionId?: string;
|
|
95
95
|
bypassCache?: boolean;
|
|
96
96
|
}
|
|
97
|
+
/**
|
|
98
|
+
* A health status value. Like the Go client's `HealthState` (a string type), an
|
|
99
|
+
* off-contract status reported by the server is preserved verbatim.
|
|
100
|
+
*/
|
|
101
|
+
export type HealthState = string;
|
|
102
|
+
/** Canonical {@link HealthStatus.status} values. */
|
|
103
|
+
export declare const HealthOK = "ok";
|
|
104
|
+
export declare const HealthDegraded = "degraded";
|
|
105
|
+
export declare const HealthUnknown = "unknown";
|
|
106
|
+
/**
|
|
107
|
+
* A snapshot of an ekoDB /api/health probe.
|
|
108
|
+
*
|
|
109
|
+
* It is degraded-tolerant: a reachable server that reports `degraded` is a
|
|
110
|
+
* successful snapshot (`reachable: true`, `status: "degraded"`), NOT an error.
|
|
111
|
+
* An unreachable/unparseable probe yields `{ reachable: false, status: "unknown" }`.
|
|
112
|
+
*
|
|
113
|
+
* Consumers base liveness on `reachable` and treat `degraded` as a warning,
|
|
114
|
+
* never as a fatal. `detail` (the full admin body, which includes internal
|
|
115
|
+
* metrics and collection names) is excluded from JSON via `toJSON()` so
|
|
116
|
+
* surfacing the snapshot cannot leak internals; read it in-process when needed.
|
|
117
|
+
*/
|
|
118
|
+
export declare class HealthStatus {
|
|
119
|
+
reachable: boolean;
|
|
120
|
+
status: HealthState;
|
|
121
|
+
integrityOk: boolean;
|
|
122
|
+
detail?: {
|
|
123
|
+
[key: string]: any;
|
|
124
|
+
};
|
|
125
|
+
constructor(init: {
|
|
126
|
+
reachable: boolean;
|
|
127
|
+
status: HealthState;
|
|
128
|
+
integrityOk: boolean;
|
|
129
|
+
detail?: {
|
|
130
|
+
[key: string]: any;
|
|
131
|
+
};
|
|
132
|
+
});
|
|
133
|
+
toJSON(): {
|
|
134
|
+
reachable: boolean;
|
|
135
|
+
status: HealthState;
|
|
136
|
+
integrity_ok: boolean;
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Interprets a parsed /api/health body per the shared health contract. A
|
|
141
|
+
* missing/odd `status` on a reachable body fails safe to `degraded`; a
|
|
142
|
+
* non-object body yields an unreachable `unknown` snapshot. `integrity_ok` is
|
|
143
|
+
* read from the top-level field (public) or nested `integrity.healthy` (admin).
|
|
144
|
+
*/
|
|
145
|
+
export declare function parseHealthStatus(body: any): HealthStatus;
|
|
97
146
|
export interface FindOptions {
|
|
98
147
|
filter?: any;
|
|
99
148
|
sort?: any;
|
|
@@ -277,12 +326,45 @@ export interface MergeSessionsRequest {
|
|
|
277
326
|
bypass_ripple?: boolean;
|
|
278
327
|
}
|
|
279
328
|
/**
|
|
280
|
-
*
|
|
329
|
+
* A provider's state on `GET /api/chat_models`. The union lists the states
|
|
330
|
+
* this client knows; the `string` escape keeps a newer server's status from
|
|
331
|
+
* failing to type-check.
|
|
332
|
+
*/
|
|
333
|
+
export type ChatProviderState = "ok" | "not_configured" | "auth_failed" | "permission_denied" | "billing" | "rate_limited" | "unavailable" | "unreachable" | "request_error" | (string & {});
|
|
334
|
+
/**
|
|
335
|
+
* One provider's row in `ChatModels.providers`.
|
|
336
|
+
*/
|
|
337
|
+
export interface ChatProviderStatus {
|
|
338
|
+
status: ChatProviderState;
|
|
339
|
+
/**
|
|
340
|
+
* True when the status is the provider's own answer about the configured
|
|
341
|
+
* key. A 5xx, a refused connection, or a missing key says nothing about it.
|
|
342
|
+
*/
|
|
343
|
+
verified: boolean;
|
|
344
|
+
/** The provider's own HTTP status, when it answered. */
|
|
345
|
+
http_status?: number;
|
|
346
|
+
/** The provider's own message, when it answered. */
|
|
347
|
+
message?: string;
|
|
348
|
+
/** How many models were listed, when the status is `ok`. */
|
|
349
|
+
model_count?: number;
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Available chat models by provider, and why each list looks the way it does.
|
|
281
353
|
*/
|
|
282
354
|
export interface ChatModels {
|
|
283
355
|
openai: string[];
|
|
284
356
|
anthropic: string[];
|
|
285
357
|
perplexity: string[];
|
|
358
|
+
/** Google Gemini models. Absent from a server that predates the field. */
|
|
359
|
+
gemini?: string[];
|
|
360
|
+
/**
|
|
361
|
+
* Per-provider status keyed by provider name. A rejected key reports
|
|
362
|
+
* `auth_failed` where a missing one reports `not_configured`, so an empty
|
|
363
|
+
* list is never ambiguous. Absent from a server that predates the map.
|
|
364
|
+
*/
|
|
365
|
+
providers?: {
|
|
366
|
+
[provider: string]: ChatProviderStatus;
|
|
367
|
+
};
|
|
286
368
|
}
|
|
287
369
|
/**
|
|
288
370
|
* Request to compact a chat session's history on demand.
|
|
@@ -861,9 +943,21 @@ export declare class EkoDBClient {
|
|
|
861
943
|
*/
|
|
862
944
|
distinctValues(collection: string, field: string, options?: DistinctValuesOptions): Promise<DistinctValuesResponse>;
|
|
863
945
|
/**
|
|
864
|
-
* Health check -
|
|
946
|
+
* Health check - reports whether the ekoDB server is reachable.
|
|
947
|
+
*
|
|
948
|
+
* Returns `true` whenever the server responds (INCLUDING when it reports
|
|
949
|
+
* `degraded`) and `false` only when it is unreachable. The ekoDB server
|
|
950
|
+
* returns HTTP 200 while degraded on purpose, so gating on `status === "ok"`
|
|
951
|
+
* would treat a degraded-but-serving server as down. Use {@link healthStatus}
|
|
952
|
+
* for the ok/degraded distinction.
|
|
865
953
|
*/
|
|
866
954
|
health(): Promise<boolean>;
|
|
955
|
+
/**
|
|
956
|
+
* Structured, degraded-tolerant health. Returns a {@link HealthStatus}
|
|
957
|
+
* snapshot; an unreachable server yields `{ reachable: false, status:
|
|
958
|
+
* "unknown" }` rather than throwing.
|
|
959
|
+
*/
|
|
960
|
+
healthStatus(): Promise<HealthStatus>;
|
|
867
961
|
/**
|
|
868
962
|
* Execute a tool via ekoDB's server-side tool pipeline.
|
|
869
963
|
*
|
|
@@ -1328,6 +1422,19 @@ export type ChatStreamEvent = {
|
|
|
1328
1422
|
} | {
|
|
1329
1423
|
type: "error";
|
|
1330
1424
|
error: string;
|
|
1425
|
+
/**
|
|
1426
|
+
* The provider-failure classification (`provider_auth_failed`,
|
|
1427
|
+
* `provider_permission_denied`, `provider_billing`,
|
|
1428
|
+
* `provider_rate_limited`, `provider_unavailable`,
|
|
1429
|
+
* `provider_unreachable`, `provider_not_configured`,
|
|
1430
|
+
* `provider_request_error`), when the failure was the LLM provider's
|
|
1431
|
+
* answer. Absent for a transport failure or a plain server error.
|
|
1432
|
+
*/
|
|
1433
|
+
errorKind?: string;
|
|
1434
|
+
provider?: string;
|
|
1435
|
+
/** The provider's own HTTP status. */
|
|
1436
|
+
providerStatus?: number;
|
|
1437
|
+
retryAfterSecs?: number;
|
|
1331
1438
|
};
|
|
1332
1439
|
/** Definition for a client-side tool the LLM can call. */
|
|
1333
1440
|
export interface ClientToolDefinition {
|
package/dist/client.js
CHANGED
|
@@ -2,41 +2,9 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* ekoDB TypeScript Client
|
|
4
4
|
*/
|
|
5
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
6
|
-
if (k2 === undefined) k2 = k;
|
|
7
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
8
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
9
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
10
|
-
}
|
|
11
|
-
Object.defineProperty(o, k2, desc);
|
|
12
|
-
}) : (function(o, m, k, k2) {
|
|
13
|
-
if (k2 === undefined) k2 = k;
|
|
14
|
-
o[k2] = m[k];
|
|
15
|
-
}));
|
|
16
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
17
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
18
|
-
}) : function(o, v) {
|
|
19
|
-
o["default"] = v;
|
|
20
|
-
});
|
|
21
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
22
|
-
var ownKeys = function(o) {
|
|
23
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
24
|
-
var ar = [];
|
|
25
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
26
|
-
return ar;
|
|
27
|
-
};
|
|
28
|
-
return ownKeys(o);
|
|
29
|
-
};
|
|
30
|
-
return function (mod) {
|
|
31
|
-
if (mod && mod.__esModule) return mod;
|
|
32
|
-
var result = {};
|
|
33
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
34
|
-
__setModuleDefault(result, mod);
|
|
35
|
-
return result;
|
|
36
|
-
};
|
|
37
|
-
})();
|
|
38
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
-
exports.WebSocketClient = exports.SchemaCache = exports.EventStream = exports.EkoDBClient = exports.MergeStrategy = exports.RateLimitError = exports.DEFAULT_REQUEST_TIMEOUT_MS = exports.SerializationFormat = void 0;
|
|
6
|
+
exports.WebSocketClient = exports.SchemaCache = exports.EventStream = exports.EkoDBClient = exports.MergeStrategy = exports.HealthStatus = exports.HealthUnknown = exports.HealthDegraded = exports.HealthOK = exports.RateLimitError = exports.DEFAULT_REQUEST_TIMEOUT_MS = exports.SerializationFormat = void 0;
|
|
7
|
+
exports.parseHealthStatus = parseHealthStatus;
|
|
40
8
|
exports.extractRecordId = extractRecordId;
|
|
41
9
|
const msgpack_1 = require("@msgpack/msgpack");
|
|
42
10
|
const query_builder_1 = require("./query-builder");
|
|
@@ -71,6 +39,74 @@ class RateLimitError extends Error {
|
|
|
71
39
|
}
|
|
72
40
|
}
|
|
73
41
|
exports.RateLimitError = RateLimitError;
|
|
42
|
+
/** Canonical {@link HealthStatus.status} values. */
|
|
43
|
+
exports.HealthOK = "ok";
|
|
44
|
+
exports.HealthDegraded = "degraded";
|
|
45
|
+
exports.HealthUnknown = "unknown";
|
|
46
|
+
/**
|
|
47
|
+
* A snapshot of an ekoDB /api/health probe.
|
|
48
|
+
*
|
|
49
|
+
* It is degraded-tolerant: a reachable server that reports `degraded` is a
|
|
50
|
+
* successful snapshot (`reachable: true`, `status: "degraded"`), NOT an error.
|
|
51
|
+
* An unreachable/unparseable probe yields `{ reachable: false, status: "unknown" }`.
|
|
52
|
+
*
|
|
53
|
+
* Consumers base liveness on `reachable` and treat `degraded` as a warning,
|
|
54
|
+
* never as a fatal. `detail` (the full admin body, which includes internal
|
|
55
|
+
* metrics and collection names) is excluded from JSON via `toJSON()` so
|
|
56
|
+
* surfacing the snapshot cannot leak internals; read it in-process when needed.
|
|
57
|
+
*/
|
|
58
|
+
class HealthStatus {
|
|
59
|
+
constructor(init) {
|
|
60
|
+
this.reachable = init.reachable;
|
|
61
|
+
this.status = init.status;
|
|
62
|
+
this.integrityOk = init.integrityOk;
|
|
63
|
+
this.detail = init.detail;
|
|
64
|
+
}
|
|
65
|
+
toJSON() {
|
|
66
|
+
return {
|
|
67
|
+
reachable: this.reachable,
|
|
68
|
+
status: this.status,
|
|
69
|
+
integrity_ok: this.integrityOk,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
exports.HealthStatus = HealthStatus;
|
|
74
|
+
/**
|
|
75
|
+
* Interprets a parsed /api/health body per the shared health contract. A
|
|
76
|
+
* missing/odd `status` on a reachable body fails safe to `degraded`; a
|
|
77
|
+
* non-object body yields an unreachable `unknown` snapshot. `integrity_ok` is
|
|
78
|
+
* read from the top-level field (public) or nested `integrity.healthy` (admin).
|
|
79
|
+
*/
|
|
80
|
+
function parseHealthStatus(body) {
|
|
81
|
+
if (body === null || typeof body !== "object" || Array.isArray(body)) {
|
|
82
|
+
return new HealthStatus({
|
|
83
|
+
reachable: false,
|
|
84
|
+
status: exports.HealthUnknown,
|
|
85
|
+
integrityOk: false,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
// Default to degraded; a non-empty string status is kept verbatim (matches
|
|
89
|
+
// the Go client). A missing/non-string status fails safe to degraded.
|
|
90
|
+
let status = exports.HealthDegraded;
|
|
91
|
+
if (typeof body.status === "string" && body.status !== "") {
|
|
92
|
+
status = body.status;
|
|
93
|
+
}
|
|
94
|
+
let integrityOk = false;
|
|
95
|
+
if (typeof body.integrity_ok === "boolean") {
|
|
96
|
+
integrityOk = body.integrity_ok;
|
|
97
|
+
}
|
|
98
|
+
else if (body.integrity &&
|
|
99
|
+
typeof body.integrity === "object" &&
|
|
100
|
+
typeof body.integrity.healthy === "boolean") {
|
|
101
|
+
integrityOk = body.integrity.healthy;
|
|
102
|
+
}
|
|
103
|
+
return new HealthStatus({
|
|
104
|
+
reachable: true,
|
|
105
|
+
status,
|
|
106
|
+
integrityOk,
|
|
107
|
+
detail: body,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
74
110
|
var MergeStrategy;
|
|
75
111
|
(function (MergeStrategy) {
|
|
76
112
|
MergeStrategy["Chronological"] = "Chronological";
|
|
@@ -1121,15 +1157,33 @@ class EkoDBClient {
|
|
|
1121
1157
|
return this.makeRequest("POST", `/api/distinct/${encodeURIComponent(collection)}/${encodeURIComponent(field)}`, body, 0, true);
|
|
1122
1158
|
}
|
|
1123
1159
|
/**
|
|
1124
|
-
* Health check -
|
|
1160
|
+
* Health check - reports whether the ekoDB server is reachable.
|
|
1161
|
+
*
|
|
1162
|
+
* Returns `true` whenever the server responds (INCLUDING when it reports
|
|
1163
|
+
* `degraded`) and `false` only when it is unreachable. The ekoDB server
|
|
1164
|
+
* returns HTTP 200 while degraded on purpose, so gating on `status === "ok"`
|
|
1165
|
+
* would treat a degraded-but-serving server as down. Use {@link healthStatus}
|
|
1166
|
+
* for the ok/degraded distinction.
|
|
1125
1167
|
*/
|
|
1126
1168
|
async health() {
|
|
1169
|
+
return (await this.healthStatus()).reachable;
|
|
1170
|
+
}
|
|
1171
|
+
/**
|
|
1172
|
+
* Structured, degraded-tolerant health. Returns a {@link HealthStatus}
|
|
1173
|
+
* snapshot; an unreachable server yields `{ reachable: false, status:
|
|
1174
|
+
* "unknown" }` rather than throwing.
|
|
1175
|
+
*/
|
|
1176
|
+
async healthStatus() {
|
|
1127
1177
|
try {
|
|
1128
|
-
const
|
|
1129
|
-
return
|
|
1178
|
+
const body = await this.makeRequest("GET", "/api/health", undefined, 0, true);
|
|
1179
|
+
return parseHealthStatus(body);
|
|
1130
1180
|
}
|
|
1131
1181
|
catch {
|
|
1132
|
-
return
|
|
1182
|
+
return new HealthStatus({
|
|
1183
|
+
reachable: false,
|
|
1184
|
+
status: exports.HealthUnknown,
|
|
1185
|
+
integrityOk: false,
|
|
1186
|
+
});
|
|
1133
1187
|
}
|
|
1134
1188
|
}
|
|
1135
1189
|
// ========== Chat Methods ==========
|
|
@@ -1367,7 +1421,22 @@ class EkoDBClient {
|
|
|
1367
1421
|
stream.close();
|
|
1368
1422
|
return;
|
|
1369
1423
|
}
|
|
1424
|
+
// The `event:` name applies to the data lines that follow it, until
|
|
1425
|
+
// the blank line that ends the frame. An error frame ends the stream:
|
|
1426
|
+
// nothing after it is surfaced and the body is not read to the end,
|
|
1427
|
+
// so a server or proxy that keeps the connection open after an error
|
|
1428
|
+
// cannot hang the caller (the Rust and Go clients stop the same way).
|
|
1429
|
+
let eventName = "";
|
|
1430
|
+
let stopped = false;
|
|
1370
1431
|
const emitLine = (line) => {
|
|
1432
|
+
if (line.startsWith("event:")) {
|
|
1433
|
+
eventName = line.slice(6).trim();
|
|
1434
|
+
return;
|
|
1435
|
+
}
|
|
1436
|
+
if (line.trim() === "") {
|
|
1437
|
+
eventName = "";
|
|
1438
|
+
return;
|
|
1439
|
+
}
|
|
1371
1440
|
if (!line.startsWith("data:"))
|
|
1372
1441
|
return;
|
|
1373
1442
|
const dataStr = line.slice(5).trim();
|
|
@@ -1375,10 +1444,16 @@ class EkoDBClient {
|
|
|
1375
1444
|
return;
|
|
1376
1445
|
try {
|
|
1377
1446
|
const eventData = JSON.parse(dataStr);
|
|
1378
|
-
|
|
1447
|
+
// An error frame is one the server names `error`, or whose
|
|
1448
|
+
// payload carries an `error`; a `message`-only payload is still
|
|
1449
|
+
// the error rather than a frame to skip, and the text is always a
|
|
1450
|
+
// string (`streamErrorText`).
|
|
1451
|
+
if (eventData.error != null || eventName === "error") {
|
|
1452
|
+
stopped = true;
|
|
1379
1453
|
stream.emit("event", {
|
|
1380
1454
|
type: "error",
|
|
1381
|
-
error: eventData
|
|
1455
|
+
error: streamErrorText(eventData),
|
|
1456
|
+
...providerFailureFields(eventData),
|
|
1382
1457
|
});
|
|
1383
1458
|
}
|
|
1384
1459
|
else if (eventData.content && eventData.message_id) {
|
|
@@ -1414,20 +1489,29 @@ class EkoDBClient {
|
|
|
1414
1489
|
break;
|
|
1415
1490
|
buffer += decoder.decode(value, { stream: true });
|
|
1416
1491
|
let nl;
|
|
1417
|
-
while ((nl = buffer.indexOf("\n")) >= 0) {
|
|
1492
|
+
while (!stopped && (nl = buffer.indexOf("\n")) >= 0) {
|
|
1418
1493
|
emitLine(buffer.slice(0, nl));
|
|
1419
1494
|
buffer = buffer.slice(nl + 1);
|
|
1420
1495
|
}
|
|
1496
|
+
if (stopped) {
|
|
1497
|
+
await reader.cancel?.()?.catch?.(() => { });
|
|
1498
|
+
break;
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
if (!stopped) {
|
|
1502
|
+
buffer += decoder.decode();
|
|
1503
|
+
if (buffer)
|
|
1504
|
+
emitLine(buffer);
|
|
1421
1505
|
}
|
|
1422
|
-
buffer += decoder.decode();
|
|
1423
|
-
if (buffer)
|
|
1424
|
-
emitLine(buffer);
|
|
1425
1506
|
}
|
|
1426
1507
|
else {
|
|
1427
1508
|
// Fallback for environments/tests without a readable body stream.
|
|
1428
1509
|
const body = await response.text();
|
|
1429
|
-
for (const line of body.split("\n"))
|
|
1510
|
+
for (const line of body.split("\n")) {
|
|
1430
1511
|
emitLine(line);
|
|
1512
|
+
if (stopped)
|
|
1513
|
+
break;
|
|
1514
|
+
}
|
|
1431
1515
|
}
|
|
1432
1516
|
stream.close();
|
|
1433
1517
|
}
|
|
@@ -2129,6 +2213,32 @@ class EkoDBClient {
|
|
|
2129
2213
|
}
|
|
2130
2214
|
}
|
|
2131
2215
|
exports.EkoDBClient = EkoDBClient;
|
|
2216
|
+
/**
|
|
2217
|
+
* The text of a stream error frame: the first of `error` / `message` that is
|
|
2218
|
+
* a non-empty string, else a fixed fallback — a structured `error` object is
|
|
2219
|
+
* still an error, never a non-string `error` on the event. Shared by the SSE
|
|
2220
|
+
* and WebSocket routes so the two cannot drift.
|
|
2221
|
+
*/
|
|
2222
|
+
function streamErrorText(payload) {
|
|
2223
|
+
const text = (value) => typeof value === "string" && value ? value : undefined;
|
|
2224
|
+
return text(payload.error) ?? text(payload.message) ?? "Unknown error";
|
|
2225
|
+
}
|
|
2226
|
+
/**
|
|
2227
|
+
* The classification fields of a stream error frame, only those present, so
|
|
2228
|
+
* a plain error stays `{ type, error }`.
|
|
2229
|
+
*/
|
|
2230
|
+
function providerFailureFields(eventData) {
|
|
2231
|
+
const fields = {};
|
|
2232
|
+
if (typeof eventData.error_kind === "string")
|
|
2233
|
+
fields.errorKind = eventData.error_kind;
|
|
2234
|
+
if (typeof eventData.provider === "string")
|
|
2235
|
+
fields.provider = eventData.provider;
|
|
2236
|
+
if (typeof eventData.provider_status === "number")
|
|
2237
|
+
fields.providerStatus = eventData.provider_status;
|
|
2238
|
+
if (typeof eventData.retry_after_secs === "number")
|
|
2239
|
+
fields.retryAfterSecs = eventData.retry_after_secs;
|
|
2240
|
+
return fields;
|
|
2241
|
+
}
|
|
2132
2242
|
/** EventEmitter-like interface for subscriptions and chat streams. */
|
|
2133
2243
|
class EventStream {
|
|
2134
2244
|
constructor() {
|
|
@@ -2342,7 +2452,7 @@ class WebSocketClient {
|
|
|
2342
2452
|
return this.connectPromise;
|
|
2343
2453
|
}
|
|
2344
2454
|
async openSocket() {
|
|
2345
|
-
const WebSocket = (await
|
|
2455
|
+
const WebSocket = (await import("ws")).default;
|
|
2346
2456
|
let url = this.wsURL;
|
|
2347
2457
|
if (!url.endsWith("/api/ws")) {
|
|
2348
2458
|
url += "/api/ws";
|
|
@@ -2674,9 +2784,12 @@ class WebSocketClient {
|
|
|
2674
2784
|
const chatId = msg.payload?.chat_id || msg.payload?.chatId;
|
|
2675
2785
|
const stream = this.chatStreams.get(chatId);
|
|
2676
2786
|
if (stream) {
|
|
2787
|
+
// The text guard and the classification are the SSE route's,
|
|
2788
|
+
// so the two routes emit the same shape.
|
|
2677
2789
|
stream.emit("event", {
|
|
2678
2790
|
type: "error",
|
|
2679
|
-
error: msg.payload
|
|
2791
|
+
error: streamErrorText(msg.payload),
|
|
2792
|
+
...providerFailureFields(msg.payload),
|
|
2680
2793
|
});
|
|
2681
2794
|
this.chatStreams.delete(chatId);
|
|
2682
2795
|
stream.close();
|