@ekodb/ekodb-client 0.25.0 → 0.26.1

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.
@@ -1045,6 +1045,47 @@ function mockErrorResponse(status, message) {
1045
1045
  (0, vitest_1.expect)(result.anthropic).toHaveLength(2);
1046
1046
  (0, vitest_1.expect)(result.perplexity).toHaveLength(1);
1047
1047
  });
1048
+ (0, vitest_1.it)("carries gemini and the per-provider status from the server", async () => {
1049
+ const client = createTestClient();
1050
+ mockTokenResponse();
1051
+ mockJsonResponse({
1052
+ openai: [],
1053
+ anthropic: ["claude-sonnet-4-5"],
1054
+ perplexity: ["sonar"],
1055
+ gemini: ["gemini-2.5-flash"],
1056
+ providers: {
1057
+ anthropic: { status: "ok", verified: true, model_count: 1 },
1058
+ gemini: { status: "ok", verified: true, model_count: 1 },
1059
+ openai: {
1060
+ status: "auth_failed",
1061
+ verified: true,
1062
+ http_status: 401,
1063
+ message: "Failed to fetch OpenAI models: 401 Unauthorized",
1064
+ },
1065
+ perplexity: {
1066
+ status: "ok",
1067
+ verified: false,
1068
+ message: "static model list; key not verified",
1069
+ },
1070
+ },
1071
+ });
1072
+ const result = await client.getChatModels();
1073
+ (0, vitest_1.expect)(result.gemini).toEqual(["gemini-2.5-flash"]);
1074
+ (0, vitest_1.expect)(result.providers?.openai.status).toBe("auth_failed");
1075
+ (0, vitest_1.expect)(result.providers?.openai.http_status).toBe(401);
1076
+ (0, vitest_1.expect)(result.providers?.openai.verified).toBe(true);
1077
+ (0, vitest_1.expect)(result.providers?.perplexity.verified).toBe(false);
1078
+ (0, vitest_1.expect)(result.providers?.anthropic.model_count).toBe(1);
1079
+ });
1080
+ (0, vitest_1.it)("tolerates a server that predates gemini and providers", async () => {
1081
+ const client = createTestClient();
1082
+ mockTokenResponse();
1083
+ mockJsonResponse({ openai: ["gpt-4o"], anthropic: [], perplexity: [] });
1084
+ const result = await client.getChatModels();
1085
+ (0, vitest_1.expect)(result.openai).toEqual(["gpt-4o"]);
1086
+ (0, vitest_1.expect)(result.gemini).toBeUndefined();
1087
+ (0, vitest_1.expect)(result.providers).toBeUndefined();
1088
+ });
1048
1089
  (0, vitest_1.it)("gets models for specific provider", async () => {
1049
1090
  const client = createTestClient();
1050
1091
  mockTokenResponse();
@@ -1909,6 +1950,112 @@ function mockErrorResponse(status, message) {
1909
1950
  (0, vitest_1.expect)(events[2].messageId).toBe("msg_1");
1910
1951
  (0, vitest_1.expect)(events[2].executionTimeMs).toBe(42);
1911
1952
  });
1953
+ (0, vitest_1.it)("treats a frame named error as an error even when its payload says message", async () => {
1954
+ const client = createTestClient();
1955
+ mockTokenResponse();
1956
+ const sseBody = 'event: token\ndata: {"token":"Hel"}\n\nevent: error\ndata: {"message":"boom"}\n\n';
1957
+ mockFetch.mockResolvedValueOnce({
1958
+ ok: true,
1959
+ status: 200,
1960
+ text: async () => sseBody,
1961
+ headers: new Headers({ "content-type": "text/event-stream" }),
1962
+ });
1963
+ const events = [];
1964
+ const stream = client.chatMessageStream("chat_123", {
1965
+ message: "Hello",
1966
+ });
1967
+ stream.on("event", (evt) => events.push(evt));
1968
+ await new Promise((resolve) => setTimeout(resolve, 50));
1969
+ (0, vitest_1.expect)(events).toEqual([
1970
+ { type: "chunk", content: "Hel" },
1971
+ { type: "error", error: "boom" },
1972
+ ]);
1973
+ });
1974
+ (0, vitest_1.it)("stops reading after an error frame and emits nothing that follows it", async () => {
1975
+ const client = createTestClient();
1976
+ mockTokenResponse();
1977
+ const sseBody = 'event: error\ndata: {"message":"boom"}\n\nevent: token\ndata: {"token":"late"}\n\n';
1978
+ mockFetch.mockResolvedValueOnce({
1979
+ ok: true,
1980
+ status: 200,
1981
+ text: async () => sseBody,
1982
+ headers: new Headers({ "content-type": "text/event-stream" }),
1983
+ });
1984
+ const events = [];
1985
+ const stream = client.chatMessageStream("chat_123", {
1986
+ message: "Hello",
1987
+ });
1988
+ stream.on("event", (evt) => events.push(evt));
1989
+ await new Promise((resolve) => setTimeout(resolve, 50));
1990
+ (0, vitest_1.expect)(events).toEqual([{ type: "error", error: "boom" }]);
1991
+ });
1992
+ (0, vitest_1.it)("cancels the body reader after an error frame instead of waiting for the server to close", async () => {
1993
+ const client = createTestClient();
1994
+ mockTokenResponse();
1995
+ const encoder = new TextEncoder();
1996
+ const chunks = [
1997
+ encoder.encode('event: error\ndata: {"message":"boom"}\n\n'),
1998
+ encoder.encode('event: token\ndata: {"token":"late"}\n\n'),
1999
+ ];
2000
+ const cancel = vitest_1.vi.fn(async () => { });
2001
+ let reads = 0;
2002
+ const reader = {
2003
+ read: vitest_1.vi.fn(async () => {
2004
+ // A server (or proxy) that does not close after the error frame: it
2005
+ // keeps sending frames. Bounded so a client that never stops fails
2006
+ // the assertions below instead of looping forever.
2007
+ if (reads >= 50)
2008
+ return { done: true, value: undefined };
2009
+ const value = chunks[Math.min(reads, chunks.length - 1)];
2010
+ reads += 1;
2011
+ return { done: false, value };
2012
+ }),
2013
+ cancel,
2014
+ };
2015
+ mockFetch.mockResolvedValueOnce({
2016
+ ok: true,
2017
+ status: 200,
2018
+ body: { getReader: () => reader },
2019
+ text: async () => "",
2020
+ headers: new Headers({ "content-type": "text/event-stream" }),
2021
+ });
2022
+ const events = [];
2023
+ const stream = client.chatMessageStream("chat_123", {
2024
+ message: "Hello",
2025
+ });
2026
+ stream.on("event", (evt) => events.push(evt));
2027
+ await new Promise((resolve) => setTimeout(resolve, 50));
2028
+ (0, vitest_1.expect)(events).toEqual([{ type: "error", error: "boom" }]);
2029
+ // One read delivered the error frame; the reader was cancelled rather
2030
+ // than read until the server closed.
2031
+ (0, vitest_1.expect)(reader.read).toHaveBeenCalledTimes(1);
2032
+ (0, vitest_1.expect)(cancel).toHaveBeenCalledTimes(1);
2033
+ });
2034
+ (0, vitest_1.it)("keeps the error text a string when the server sends a structured error", async () => {
2035
+ const client = createTestClient();
2036
+ mockTokenResponse();
2037
+ const sseBody = 'event: error\ndata: {"error":{"code":"upstream_down","status":503},"error_kind":"provider_unavailable","provider":"openai"}\n\n';
2038
+ mockFetch.mockResolvedValueOnce({
2039
+ ok: true,
2040
+ status: 200,
2041
+ text: async () => sseBody,
2042
+ headers: new Headers({ "content-type": "text/event-stream" }),
2043
+ });
2044
+ const events = [];
2045
+ const stream = client.chatMessageStream("chat_123", {
2046
+ message: "Hello",
2047
+ });
2048
+ stream.on("event", (evt) => events.push(evt));
2049
+ await new Promise((resolve) => setTimeout(resolve, 50));
2050
+ (0, vitest_1.expect)(events).toEqual([
2051
+ {
2052
+ type: "error",
2053
+ error: "Unknown error",
2054
+ errorKind: "provider_unavailable",
2055
+ provider: "openai",
2056
+ },
2057
+ ]);
2058
+ });
1912
2059
  (0, vitest_1.it)("emits error event on SSE error", async () => {
1913
2060
  const client = createTestClient();
1914
2061
  mockTokenResponse();
@@ -1928,6 +2075,36 @@ function mockErrorResponse(status, message) {
1928
2075
  (0, vitest_1.expect)(events).toHaveLength(1);
1929
2076
  (0, vitest_1.expect)(events[0]).toEqual({ type: "error", error: "LLM timeout" });
1930
2077
  });
2078
+ (0, vitest_1.it)("carries the provider failure classification on an error event", async () => {
2079
+ // The deployment classifies a provider failure (`error_kind`, `provider`,
2080
+ // `provider_status`, `retry_after_secs` on the wire); the event carries
2081
+ // every one of them, in this shape's camelCase, so a consumer can act on
2082
+ // it without string-matching.
2083
+ const client = createTestClient();
2084
+ mockTokenResponse();
2085
+ const sseBody = 'data: {"error":"OpenAI API error: Incorrect API key provided","error_kind":"provider_auth_failed","provider":"openai","provider_status":401}\n';
2086
+ mockFetch.mockResolvedValueOnce({
2087
+ ok: true,
2088
+ status: 200,
2089
+ text: async () => sseBody,
2090
+ headers: new Headers({ "content-type": "text/event-stream" }),
2091
+ });
2092
+ const events = [];
2093
+ const stream = client.chatMessageStream("chat_123", {
2094
+ message: "Hello",
2095
+ });
2096
+ stream.on("event", (evt) => events.push(evt));
2097
+ await new Promise((resolve) => setTimeout(resolve, 50));
2098
+ (0, vitest_1.expect)(events).toEqual([
2099
+ {
2100
+ type: "error",
2101
+ error: "OpenAI API error: Incorrect API key provided",
2102
+ errorKind: "provider_auth_failed",
2103
+ provider: "openai",
2104
+ providerStatus: 401,
2105
+ },
2106
+ ]);
2107
+ });
1931
2108
  (0, vitest_1.it)("emits error event on non-200 HTTP response", async () => {
1932
2109
  const client = createTestClient();
1933
2110
  mockTokenResponse();
@@ -2100,6 +2277,15 @@ function mockErrorResponse(status, message) {
2100
2277
  mockJsonResponse({ id: "sched_1", status: "paused" });
2101
2278
  const result = await client.pauseSchedule("sched_1");
2102
2279
  (0, vitest_1.expect)(result).toHaveProperty("status", "paused");
2280
+ // Assert the REQUEST. There is no /pause route; pausing is a partial
2281
+ // update of `enabled`. A response-only assertion passed for as long as
2282
+ // this method POSTed to a route that does not exist.
2283
+ const calls = global.fetch.mock.calls;
2284
+ const dataCall = calls[1]; // calls[0] is the token exchange
2285
+ (0, vitest_1.expect)(dataCall[0]).toContain("/api/schedules/sched_1");
2286
+ (0, vitest_1.expect)(dataCall[0]).not.toContain("/pause");
2287
+ (0, vitest_1.expect)(dataCall[1]?.method).toBe("PUT");
2288
+ (0, vitest_1.expect)(JSON.parse(dataCall[1]?.body)).toEqual({ enabled: false });
2103
2289
  });
2104
2290
  (0, vitest_1.it)("resumes a schedule", async () => {
2105
2291
  const client = createTestClient();
@@ -2107,6 +2293,12 @@ function mockErrorResponse(status, message) {
2107
2293
  mockJsonResponse({ id: "sched_1", status: "active" });
2108
2294
  const result = await client.resumeSchedule("sched_1");
2109
2295
  (0, vitest_1.expect)(result).toHaveProperty("status", "active");
2296
+ const calls = global.fetch.mock.calls;
2297
+ const dataCall = calls[1];
2298
+ (0, vitest_1.expect)(dataCall[0]).toContain("/api/schedules/sched_1");
2299
+ (0, vitest_1.expect)(dataCall[0]).not.toContain("/resume");
2300
+ (0, vitest_1.expect)(dataCall[1]?.method).toBe("PUT");
2301
+ (0, vitest_1.expect)(JSON.parse(dataCall[1]?.body)).toEqual({ enabled: true });
2110
2302
  });
2111
2303
  });
2112
2304
  // ============================================================================
@@ -2124,6 +2316,14 @@ function mockErrorResponse(status, message) {
2124
2316
  });
2125
2317
  const result = await client.kvGetLinks("session:user123");
2126
2318
  (0, vitest_1.expect)(result).toHaveProperty("links");
2319
+ // Assert the REQUEST, not just the mocked response. These three methods
2320
+ // shipped pointing at routes that do not exist, and every one of these
2321
+ // tests passed the whole time, because a mocked response says nothing
2322
+ // about the URL the client actually asked for.
2323
+ const calls = global.fetch.mock.calls;
2324
+ const dataCall = calls[1]; // calls[0] is the token exchange
2325
+ (0, vitest_1.expect)(dataCall[0]).toContain("/api/kv/session%3Auser123/links");
2326
+ (0, vitest_1.expect)(dataCall[1]?.method).toBe("GET");
2127
2327
  });
2128
2328
  (0, vitest_1.it)("links a document to a KV key", async () => {
2129
2329
  const client = createTestClient();
@@ -2131,6 +2331,26 @@ function mockErrorResponse(status, message) {
2131
2331
  mockJsonResponse({ status: "linked" });
2132
2332
  const result = await client.kvLink("session:user123", "users", "user_1");
2133
2333
  (0, vitest_1.expect)(result).toHaveProperty("status", "linked");
2334
+ const calls = global.fetch.mock.calls;
2335
+ const dataCall = calls[1];
2336
+ // The identifying triple belongs in the PATH, not the body.
2337
+ (0, vitest_1.expect)(dataCall[0]).toContain("/api/kv/session%3Auser123/links/users/user_1");
2338
+ (0, vitest_1.expect)(dataCall[1]?.method).toBe("POST");
2339
+ });
2340
+ (0, vitest_1.it)("passes optional link data in the body", async () => {
2341
+ const client = createTestClient();
2342
+ mockTokenResponse();
2343
+ mockJsonResponse({ status: "linked" });
2344
+ await client.kvLink("session:user123", "users", "user_1", {
2345
+ field_path: "profile.avatar",
2346
+ metadata: { source: "signup" },
2347
+ });
2348
+ const calls = global.fetch.mock.calls;
2349
+ const body = JSON.parse(calls[1][1]?.body);
2350
+ (0, vitest_1.expect)(body).toEqual({
2351
+ field_path: "profile.avatar",
2352
+ metadata: { source: "signup" },
2353
+ });
2134
2354
  });
2135
2355
  (0, vitest_1.it)("unlinks a document from a KV key", async () => {
2136
2356
  const client = createTestClient();
@@ -2138,6 +2358,11 @@ function mockErrorResponse(status, message) {
2138
2358
  mockJsonResponse({ status: "unlinked" });
2139
2359
  const result = await client.kvUnlink("session:user123", "users", "user_1");
2140
2360
  (0, vitest_1.expect)(result).toHaveProperty("status", "unlinked");
2361
+ const calls = global.fetch.mock.calls;
2362
+ const dataCall = calls[1];
2363
+ (0, vitest_1.expect)(dataCall[0]).toContain("/api/kv/session%3Auser123/links/users/user_1");
2364
+ // DELETE, not POST — the previous implementation used POST and 404'd.
2365
+ (0, vitest_1.expect)(dataCall[1]?.method).toBe("DELETE");
2141
2366
  });
2142
2367
  });
2143
2368
  // ============================================================================
@@ -2161,7 +2386,7 @@ function mockErrorResponse(status, message) {
2161
2386
  },
2162
2387
  ],
2163
2388
  total: 2,
2164
- took_ms: 12,
2389
+ execution_time_ms: 12,
2165
2390
  });
2166
2391
  const result = await client.textSearch("documents", "ownership", {
2167
2392
  limit: 10,
@@ -2183,7 +2408,7 @@ function mockErrorResponse(status, message) {
2183
2408
  },
2184
2409
  ],
2185
2410
  total: 1,
2186
- took_ms: 25,
2411
+ execution_time_ms: 25,
2187
2412
  });
2188
2413
  const queryVector = [0.1, 0.2, 0.3, 0.4, 0.5];
2189
2414
  const result = await client.hybridSearch("documents", "machine learning", queryVector, 5);
@@ -56,18 +56,6 @@ export type FunctionStageConfig = {
56
56
  } | {
57
57
  type: "Count";
58
58
  output_field: string;
59
- } | {
60
- type: "Filter";
61
- filter: Record<string, any>;
62
- } | {
63
- type: "Sort";
64
- sort: SortFieldConfig[];
65
- } | {
66
- type: "Limit";
67
- limit: number;
68
- } | {
69
- type: "Skip";
70
- skip: number;
71
59
  } | {
72
60
  type: "Insert";
73
61
  collection: string;
@@ -581,10 +569,25 @@ export declare const Stage: {
581
569
  deleteById: (collection: string, record_id: string, bypassRipple?: boolean) => FunctionStageConfig;
582
570
  batchInsert: (collection: string, records: Record<string, any>[], bypassRipple?: boolean) => FunctionStageConfig;
583
571
  batchDelete: (collection: string, record_ids: string[], bypassRipple?: boolean) => FunctionStageConfig;
584
- filter: (filter: Record<string, any>) => FunctionStageConfig;
585
- sort: (sort: SortFieldConfig[]) => FunctionStageConfig;
586
- limit: (limit: number) => FunctionStageConfig;
587
- skip: (skip: number) => FunctionStageConfig;
572
+ /**
573
+ * Filter a collection.
574
+ *
575
+ * Shorthand for a `Query` stage carrying only `filter`. There is no separate
576
+ * `Filter` stage server-side — filtering, sorting, limiting and skipping are
577
+ * all fields on `Query`. This previously emitted `{ type: "Filter" }`, which
578
+ * the server has no variant for; because a function's stage array
579
+ * deserializes as a unit, one such stage rejected the ENTIRE function.
580
+ *
581
+ * Use {@link Stage.query} when you need more than one of these at once — it
582
+ * takes them together and produces a single stage.
583
+ */
584
+ filter: (collection: string, filter: Record<string, any>) => FunctionStageConfig;
585
+ /** Sort a collection. Shorthand for a `Query` carrying only `sort`. */
586
+ sort: (collection: string, sort: SortFieldConfig[]) => FunctionStageConfig;
587
+ /** Limit a collection read. Shorthand for a `Query` carrying only `limit`. */
588
+ limit: (collection: string, limit: number) => FunctionStageConfig;
589
+ /** Skip rows of a collection read. Shorthand for a `Query` with only `skip`. */
590
+ skip: (collection: string, skip: number) => FunctionStageConfig;
588
591
  httpRequest: (url: string, method?: string, headers?: Record<string, string>, body?: any) => FunctionStageConfig;
589
592
  vectorSearch: (collection: string, query_vector: number[], limit?: number, threshold?: number) => FunctionStageConfig;
590
593
  textSearch: (collection: string, query_text: string, options?: {
package/dist/functions.js CHANGED
@@ -103,20 +103,39 @@ exports.Stage = {
103
103
  record_ids,
104
104
  bypass_ripple: bypassRipple,
105
105
  }),
106
- filter: (filter) => ({
107
- type: "Filter",
106
+ /**
107
+ * Filter a collection.
108
+ *
109
+ * Shorthand for a `Query` stage carrying only `filter`. There is no separate
110
+ * `Filter` stage server-side — filtering, sorting, limiting and skipping are
111
+ * all fields on `Query`. This previously emitted `{ type: "Filter" }`, which
112
+ * the server has no variant for; because a function's stage array
113
+ * deserializes as a unit, one such stage rejected the ENTIRE function.
114
+ *
115
+ * Use {@link Stage.query} when you need more than one of these at once — it
116
+ * takes them together and produces a single stage.
117
+ */
118
+ filter: (collection, filter) => ({
119
+ type: "Query",
120
+ collection,
108
121
  filter,
109
122
  }),
110
- sort: (sort) => ({
111
- type: "Sort",
123
+ /** Sort a collection. Shorthand for a `Query` carrying only `sort`. */
124
+ sort: (collection, sort) => ({
125
+ type: "Query",
126
+ collection,
112
127
  sort,
113
128
  }),
114
- limit: (limit) => ({
115
- type: "Limit",
129
+ /** Limit a collection read. Shorthand for a `Query` carrying only `limit`. */
130
+ limit: (collection, limit) => ({
131
+ type: "Query",
132
+ collection,
116
133
  limit,
117
134
  }),
118
- skip: (skip) => ({
119
- type: "Skip",
135
+ /** Skip rows of a collection read. Shorthand for a `Query` with only `skip`. */
136
+ skip: (collection, skip) => ({
137
+ type: "Query",
138
+ collection,
120
139
  skip,
121
140
  }),
122
141
  httpRequest: (url, method = "GET", headers, body) => ({
@@ -538,4 +538,46 @@ const functions_1 = require("./functions");
538
538
  (0, vitest_1.expect)(wire.type).toBe(s.type);
539
539
  }
540
540
  });
541
+ (0, vitest_1.describe)("filter/sort/limit/skip emit Query stages", () => {
542
+ // These four used to emit { type: "Filter" | "Sort" | "Limit" | "Skip" },
543
+ // none of which the server has a variant for. Because a function's stage
544
+ // array deserializes as a unit, a single one of them rejected the ENTIRE
545
+ // function. They are shorthands for a Query carrying that one field.
546
+ (0, vitest_1.it)("filter emits a Query with only the filter set", () => {
547
+ const wire = JSON.parse(JSON.stringify(functions_1.Stage.filter("users", { status: "active" })));
548
+ (0, vitest_1.expect)(wire.type).toBe("Query");
549
+ (0, vitest_1.expect)(wire.collection).toBe("users");
550
+ (0, vitest_1.expect)(wire.filter).toEqual({ status: "active" });
551
+ });
552
+ (0, vitest_1.it)("sort emits a Query with only the sort set", () => {
553
+ const wire = JSON.parse(JSON.stringify(functions_1.Stage.sort("users", [{ field: "created_at", ascending: false }])));
554
+ (0, vitest_1.expect)(wire.type).toBe("Query");
555
+ (0, vitest_1.expect)(wire.collection).toBe("users");
556
+ (0, vitest_1.expect)(wire.sort).toEqual([{ field: "created_at", ascending: false }]);
557
+ });
558
+ (0, vitest_1.it)("limit emits a Query with only the limit set", () => {
559
+ const wire = JSON.parse(JSON.stringify(functions_1.Stage.limit("users", 10)));
560
+ (0, vitest_1.expect)(wire.type).toBe("Query");
561
+ (0, vitest_1.expect)(wire.collection).toBe("users");
562
+ (0, vitest_1.expect)(wire.limit).toBe(10);
563
+ });
564
+ (0, vitest_1.it)("skip emits a Query with only the skip set", () => {
565
+ const wire = JSON.parse(JSON.stringify(functions_1.Stage.skip("users", 5)));
566
+ (0, vitest_1.expect)(wire.type).toBe("Query");
567
+ (0, vitest_1.expect)(wire.collection).toBe("users");
568
+ (0, vitest_1.expect)(wire.skip).toBe(5);
569
+ });
570
+ (0, vitest_1.it)("never emits a stage type the server has no variant for", () => {
571
+ const wire = [
572
+ functions_1.Stage.filter("users", {}),
573
+ functions_1.Stage.sort("users", []),
574
+ functions_1.Stage.limit("users", 1),
575
+ functions_1.Stage.skip("users", 1),
576
+ ].map((s) => JSON.parse(JSON.stringify(s)).type);
577
+ (0, vitest_1.expect)(wire).not.toContain("Filter");
578
+ (0, vitest_1.expect)(wire).not.toContain("Sort");
579
+ (0, vitest_1.expect)(wire).not.toContain("Limit");
580
+ (0, vitest_1.expect)(wire).not.toContain("Skip");
581
+ });
582
+ });
541
583
  });
package/dist/index.d.ts CHANGED
@@ -13,4 +13,4 @@ export type { Schema, FieldTypeSchema, IndexConfig, CollectionMetadata, } from "
13
13
  export type { JoinConfig } from "./join";
14
14
  export type { UserFunction, ParameterDefinition, FunctionStageConfig, GroupFunctionConfig, SortFieldConfig, FunctionResult, FunctionStats, StageStats, } from "./functions";
15
15
  export type { MutationNotification, ChatStreamEvent, ClientToolDefinition, ChatSendOptions, SubscribeOptions, } from "./client";
16
- export type { Record, Query, BatchOperationResult, ClientConfig, RateLimitInfo, CollectionConfig, ChatRequest, CreateChatSessionRequest, ChatMessageRequest, TokenUsage, ChatResponse, ChatSession, ChatSessionResponse, ListSessionsQuery, ListSessionsResponse, GetMessagesQuery, GetMessagesResponse, UpdateSessionRequest, MergeSessionsRequest, ChatModels, CompactChatRequest, CompactChatResponse, EmbedRequest, EmbedResponse, RawCompletionRequest, RawCompletionResponse, ToolChoice, ToolConfig, } from "./client";
16
+ export type { Record, Query, BatchOperationResult, ClientConfig, RateLimitInfo, CollectionConfig, ChatRequest, CreateChatSessionRequest, ChatMessageRequest, TokenUsage, ChatResponse, ChatSession, ChatSessionResponse, ListSessionsQuery, ListSessionsResponse, GetMessagesQuery, GetMessagesResponse, UpdateSessionRequest, MergeSessionsRequest, ChatModels, ChatProviderState, ChatProviderStatus, CompactChatRequest, CompactChatResponse, EmbedRequest, EmbedResponse, RawCompletionRequest, RawCompletionResponse, ToolChoice, ToolConfig, } from "./client";
package/dist/search.d.ts CHANGED
@@ -79,7 +79,7 @@ export interface SearchResponse {
79
79
  /** Total number of results found */
80
80
  total: number;
81
81
  /** Query execution time in milliseconds */
82
- took_ms?: number;
82
+ execution_time_ms?: number;
83
83
  }
84
84
  /**
85
85
  * Builder for constructing search queries with fluent API
@@ -300,6 +300,72 @@ function waitForMessage(ws) {
300
300
  (0, vitest_1.expect)(stream.closed).toBe(true);
301
301
  client.close();
302
302
  });
303
+ // The WebSocket route carries the provider-failure classification like
304
+ // the SSE route does, in the event's camelCase shape.
305
+ (0, vitest_1.it)("carries the provider failure classification on a chat stream error", async () => {
306
+ const client = new client_1.WebSocketClient(`ws://localhost:${port}/api/ws`, "test-token");
307
+ const streamPromise = client.chatSend("chat-4", "test");
308
+ await new Promise((r) => wss.once("connection", r));
309
+ const ws = getLastConnection();
310
+ await waitForMessage(ws);
311
+ const stream = await streamPromise;
312
+ const events = [];
313
+ stream.on("event", (e) => events.push(e));
314
+ ws.send(JSON.stringify({
315
+ type: "ChatStreamError",
316
+ payload: {
317
+ chat_id: "chat-4",
318
+ error: "OpenAI API error 429 Too Many Requests",
319
+ error_kind: "provider_rate_limited",
320
+ provider: "openai",
321
+ provider_status: 429,
322
+ retry_after_secs: 7,
323
+ },
324
+ }));
325
+ await new Promise((r) => stream.on("close", r));
326
+ (0, vitest_1.expect)(events).toEqual([
327
+ {
328
+ type: "error",
329
+ error: "OpenAI API error 429 Too Many Requests",
330
+ errorKind: "provider_rate_limited",
331
+ provider: "openai",
332
+ providerStatus: 429,
333
+ retryAfterSecs: 7,
334
+ },
335
+ ]);
336
+ client.close();
337
+ });
338
+ // A structured `error` value is still an error, with string text — the
339
+ // WebSocket route guards the shape exactly as the SSE route does.
340
+ (0, vitest_1.it)("keeps the error text a string when the payload's error is an object", async () => {
341
+ const client = new client_1.WebSocketClient(`ws://localhost:${port}/api/ws`, "test-token");
342
+ const streamPromise = client.chatSend("chat-5", "test");
343
+ await new Promise((r) => wss.once("connection", r));
344
+ const ws = getLastConnection();
345
+ await waitForMessage(ws);
346
+ const stream = await streamPromise;
347
+ const events = [];
348
+ stream.on("event", (e) => events.push(e));
349
+ ws.send(JSON.stringify({
350
+ type: "ChatStreamError",
351
+ payload: {
352
+ chat_id: "chat-5",
353
+ error: { code: "upstream_down", status: 503 },
354
+ error_kind: "provider_unavailable",
355
+ provider: "gemini",
356
+ },
357
+ }));
358
+ await new Promise((r) => stream.on("close", r));
359
+ (0, vitest_1.expect)(events).toEqual([
360
+ {
361
+ type: "error",
362
+ error: "Unknown error",
363
+ errorKind: "provider_unavailable",
364
+ provider: "gemini",
365
+ },
366
+ ]);
367
+ client.close();
368
+ });
303
369
  (0, vitest_1.it)("sends options with ChatSend", async () => {
304
370
  const client = new client_1.WebSocketClient(`ws://localhost:${port}/api/ws`, "test-token");
305
371
  const streamPromise = client.chatSend("chat-3", "Hello", {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ekodb/ekodb-client",
3
- "version": "0.25.0",
3
+ "version": "0.26.1",
4
4
  "description": "Official TypeScript/JavaScript client for ekoDB",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -20,9 +20,9 @@
20
20
  "author": "ekoDB",
21
21
  "license": "MIT",
22
22
  "devDependencies": {
23
- "@types/node": "^24.13.2",
23
+ "@types/node": "^26.4.0",
24
24
  "@types/ws": "^8.18.1",
25
- "typescript": "^5.9.3",
25
+ "typescript": "^6.0.3",
26
26
  "vitest": "^4.0.18"
27
27
  },
28
28
  "dependencies": {