@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 CHANGED
@@ -308,8 +308,11 @@ const joinResults = await client.find("users", multiQuery);
308
308
 
309
309
  #### Chat Models
310
310
 
311
- - `getChatModels(): Promise<Record<string, string[]>>` - Get all available chat
312
- models by provider
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
- * Available chat models by provider
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 - verify the ekoDB server is responding
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 - verify the ekoDB server is responding
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 result = await this.makeRequest("GET", "/api/health", undefined, 0, true);
1129
- return result.status === "ok";
1178
+ const body = await this.makeRequest("GET", "/api/health", undefined, 0, true);
1179
+ return parseHealthStatus(body);
1130
1180
  }
1131
1181
  catch {
1132
- return false;
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
- if (eventData.error) {
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.error,
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 Promise.resolve().then(() => __importStar(require("ws")))).default;
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.error || msg.payload.message || "Unknown error",
2791
+ error: streamErrorText(msg.payload),
2792
+ ...providerFailureFields(msg.payload),
2680
2793
  });
2681
2794
  this.chatStreams.delete(chatId);
2682
2795
  stream.close();