okengine 0.12.0 → 0.14.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.
- package/manifest.v1.schema.json +7 -1
- package/package.json +2 -2
- package/site/content/docs/ai/mcp.mdx +27 -2
- package/site/content/docs/elements/ai.mdx +58 -11
- package/site/content/docs/elements/clock.mdx +27 -12
- package/site/content/docs/elements/flow.mdx +6 -3
- package/site/content/docs/elements/signal.mdx +71 -25
- package/site/content/docs/elements/store.mdx +21 -1
- package/site/content/docs/get-started/installation.mdx +2 -2
- package/site/content/docs/providers/index.mdx +1 -0
- package/site/content/docs/recipes/dragonfly.mdx +4 -3
- package/site/content/docs/recipes/redis.mdx +4 -5
- package/site/content/docs/recipes/valkey.mdx +5 -3
- package/site/content/docs/reference/configuration.mdx +3 -1
- package/site/content/docs/reference/environment-variables.mdx +5 -5
- package/site/content/docs/reference/fx.mdx +27 -22
- package/src/cli/ai-setup/recommend.test.ts +25 -0
- package/src/cli/ai-setup/recommend.ts +8 -3
- package/src/cli/load-config.images.test.ts +1 -0
- package/src/compiler/effects-infer.ts +58 -3
- package/src/compiler/extract.test.ts +68 -1
- package/src/compiler/extract.ts +70 -2
- package/src/compiler/response.ts +45 -1
- package/src/console/server/ai.ts +5 -2
- package/src/console/server/flows.ts +1 -0
- package/src/console/server/serve.ts +17 -2
- package/src/console/ui-next/dist/assets/cache-glyph-BanhLsEY.js +1 -0
- package/src/console/ui-next/dist/assets/flows-page-DxDsOd4f.js +1 -0
- package/src/console/ui-next/dist/assets/http-method-CJCBYL2j.js +1 -0
- package/src/console/ui-next/dist/assets/{index-DX248G39.js → index-Ce6WKWKM.js} +3 -3
- package/src/console/ui-next/dist/assets/observability-page-BEZDzyYh.js +4 -0
- package/src/console/ui-next/dist/assets/trace-detail-sheet-DFLFfUUX.js +2 -0
- package/src/console/ui-next/dist/assets/units-page-C0gW6Kdo.js +1 -0
- package/src/console/ui-next/dist/assets/{vault-page-CSOYMHhO.js → vault-page-DuKzqwzW.js} +1 -1
- package/src/console/ui-next/dist/index.html +1 -1
- package/src/console/ui-next/seed-invoke-host.ts +2 -0
- package/src/console/ui-next/src/features/flows/graph/build-flow-graph.test.ts +18 -0
- package/src/console/ui-next/src/features/flows/graph/build-flow-graph.ts +14 -1
- package/src/console/ui-next/src/features/flows/graph/neighborhood.test.ts +17 -0
- package/src/console/ui-next/src/features/flows/graph/neighborhood.ts +16 -3
- package/src/console/ui-next/src/features/flows/traces/effect-kind.ts +3 -1
- package/src/console/ui-next/src/features/flows/traces/effect-summary.ts +24 -0
- package/src/console/ui-next/src/features/flows/traces/trace-detail-sheet.tsx +9 -3
- package/src/console/ui-next/src/features/flows/traces/trace-detail.test.ts +10 -1
- package/src/console/ui-next/src/features/observability/lib/ask-count.test.ts +25 -0
- package/src/console/ui-next/src/features/observability/lib/ask-count.ts +4 -1
- package/src/console/ui-next/src/features/units/detail/effects-summary.tsx +12 -4
- package/src/docker/docker.test.ts +1 -1
- package/src/docker/dockerfile.ts +1 -1
- package/src/docker/helpers.ts +28 -0
- package/src/docker/recipes/dragonfly.ts +3 -6
- package/src/docker/recipes/redis.ts +2 -6
- package/src/docker/recipes/valkey.ts +2 -6
- package/src/drivers/ai-anthropic.ts +5 -0
- package/src/drivers/ai-ollama.ts +49 -30
- package/src/drivers/ai-openai-compatible.ts +57 -46
- package/src/drivers/ai-providers.test.ts +3 -0
- package/src/drivers/bun-native-completeness.test.ts +7 -9
- package/src/drivers/redis.ts +11 -4
- package/src/drivers/signal-redis.ts +24 -14
- package/src/drivers/signal-types.ts +2 -1
- package/src/drivers/types.ts +1 -1
- package/src/elements/ai/declare.ts +109 -0
- package/src/elements/ai/errors.test.ts +5 -1
- package/src/elements/ai/errors.ts +30 -2
- package/src/elements/ai/eval.ts +4 -6
- package/src/elements/ai/mcp-client.test.ts +206 -0
- package/src/elements/ai/mcp-client.ts +362 -0
- package/src/elements/ai/mcp-http.ts +159 -0
- package/src/elements/ai/mcp-mock.ts +134 -0
- package/src/elements/ai/mcp-protocol.ts +234 -0
- package/src/elements/ai/mcp-stdio.test.ts +50 -0
- package/src/elements/ai/mcp-stdio.ts +212 -0
- package/src/elements/ai/mcp-transport.ts +70 -0
- package/src/elements/ai/runtime.ts +159 -29
- package/src/elements/ai.test.ts +139 -0
- package/src/elements/ai.ts +16 -0
- package/src/elements/clock/health.test.ts +43 -0
- package/src/elements/clock/runtime.ts +55 -2
- package/src/elements/clock/schedule.ts +71 -142
- package/src/elements/clock.test.ts +9 -40
- package/src/elements/clock.ts +6 -1
- package/src/elements/gate/runtime.ts +3 -3
- package/src/elements/index.ts +2 -0
- package/src/elements/signal/declare.ts +2 -1
- package/src/elements/signal/runtime.ts +16 -1
- package/src/elements/signal.ts +1 -0
- package/src/elements/store/cache.test.ts +2 -0
- package/src/elements/store/cache.ts +3 -3
- package/src/elements/store/declare.ts +7 -0
- package/src/elements/store/kv-sql.test.ts +86 -0
- package/src/elements/store/kv-sql.ts +178 -0
- package/src/elements/store/runtime.ts +35 -9
- package/src/elements/store.test.ts +2 -0
- package/src/elements/vault/builtin-adapter.ts +17 -0
- package/src/full.ts +2 -0
- package/src/index.ts +4 -0
- package/src/kernel/app.ts +97 -64
- package/src/kernel/auto-registry.test.ts +5 -0
- package/src/kernel/boot-bind/ai.ts +24 -0
- package/src/kernel/boot-bind/clock.ts +2 -0
- package/src/kernel/boot-bind/store.test.ts +150 -1
- package/src/kernel/boot-bind/store.ts +66 -1
- package/src/kernel/boot.ts +3 -1
- package/src/kernel/element-registries.ts +4 -1
- package/src/kernel/fx-dead-letters.test.ts +77 -0
- package/src/kernel/fx.test.ts +22 -0
- package/src/kernel/fx.ts +112 -6
- package/src/kernel/http-stream.test.ts +174 -0
- package/src/kernel/index.ts +2 -0
- package/src/manifest/diff.test.ts +13 -0
- package/src/manifest/diff.ts +15 -0
- package/src/manifest/mcp-ref.ts +88 -0
- package/src/manifest/types.ts +33 -2
- package/src/manifest/validate.test.ts +20 -0
- package/src/mcp/docs-server.ts +1 -1
- package/src/mcp/server.ts +1 -1
- package/src/plugins/compression.test.ts +21 -0
- package/src/plugins/compression.ts +1 -0
- package/src/runtime/bun.ts +41 -4
- package/src/test/reset-element-registries.ts +2 -0
- package/src/console/ui-next/dist/assets/cache-glyph-CLPBqZeb.js +0 -1
- package/src/console/ui-next/dist/assets/flows-page-C_Tas1E1.js +0 -1
- package/src/console/ui-next/dist/assets/http-method-BJ92Z_ke.js +0 -1
- package/src/console/ui-next/dist/assets/observability-page-BwVS5Jvm.js +0 -4
- package/src/console/ui-next/dist/assets/trace-detail-sheet-D16lWQMt.js +0 -2
- package/src/console/ui-next/dist/assets/units-page-C5KOI7qG.js +0 -1
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* Official image: `docker.dragonflydb.io/dragonflydb/dragonfly`.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import { dragonflyServerCommand } from "../helpers.ts";
|
|
8
9
|
import type { ImageRecipe } from "../types.ts";
|
|
9
10
|
|
|
10
11
|
/** Dragonfly — Redis protocol on 6379. */
|
|
@@ -12,13 +13,9 @@ export const dragonfly: ImageRecipe = {
|
|
|
12
13
|
id: "dragonfly",
|
|
13
14
|
port: 6379,
|
|
14
15
|
match: (i) => /dragonfly/i.test(i),
|
|
15
|
-
apply: () => ({
|
|
16
|
+
apply: (s) => ({
|
|
16
17
|
environment: { HEALTHCHECK_PORT: "6379" },
|
|
17
|
-
command: [
|
|
18
|
-
"sh",
|
|
19
|
-
"-c",
|
|
20
|
-
'exec dragonfly --requirepass "$$OKE_STORE_KV_PASSWORD" --maxmemory "$${OKE_STORE_KV_MAXMEMORY:-0}"',
|
|
21
|
-
],
|
|
18
|
+
command: ["sh", "-c", dragonflyServerCommand(s)],
|
|
22
19
|
ulimits: { memlock: -1 },
|
|
23
20
|
healthcheck: {
|
|
24
21
|
test: ["CMD", "/usr/local/bin/healthcheck.sh"],
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* `redis://` URL, different binaries. Pin via `images["store.kv"]`.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { credEnv } from "../helpers.ts";
|
|
8
|
+
import { credEnv, kvServerCommand } from "../helpers.ts";
|
|
9
9
|
import type { ImageRecipe } from "../types.ts";
|
|
10
10
|
|
|
11
11
|
/** Redis Open Source — Redis protocol on 6379. */
|
|
@@ -14,11 +14,7 @@ export const redis: ImageRecipe = {
|
|
|
14
14
|
port: 6379,
|
|
15
15
|
match: (i) => /(?:^|\/)redis(?:[:@/]|$)/i.test(i) || /keydb/i.test(i),
|
|
16
16
|
apply: (s) => ({
|
|
17
|
-
command: [
|
|
18
|
-
"sh",
|
|
19
|
-
"-c",
|
|
20
|
-
'exec redis-server --requirepass "$$OKE_STORE_KV_PASSWORD" --maxmemory "$${OKE_STORE_KV_MAXMEMORY:-0}" --maxmemory-policy "$${OKE_STORE_KV_MAXMEMORY_POLICY:-noeviction}"',
|
|
21
|
-
],
|
|
17
|
+
command: ["sh", "-c", kvServerCommand(s, "redis-server")],
|
|
22
18
|
healthcheck: {
|
|
23
19
|
test: ["CMD", "redis-cli", "-a", credEnv(s, "PASSWORD"), "ping"],
|
|
24
20
|
interval: "5s",
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* `valkey/valkey`.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { credEnv } from "../helpers.ts";
|
|
8
|
+
import { credEnv, kvServerCommand } from "../helpers.ts";
|
|
9
9
|
import type { ImageRecipe } from "../types.ts";
|
|
10
10
|
|
|
11
11
|
/** Valkey — Redis protocol on 6379. */
|
|
@@ -14,11 +14,7 @@ export const valkey: ImageRecipe = {
|
|
|
14
14
|
port: 6379,
|
|
15
15
|
match: (i) => /valkey/i.test(i),
|
|
16
16
|
apply: (s) => ({
|
|
17
|
-
command: [
|
|
18
|
-
"sh",
|
|
19
|
-
"-c",
|
|
20
|
-
'exec valkey-server --requirepass "$$OKE_STORE_KV_PASSWORD" --maxmemory "$${OKE_STORE_KV_MAXMEMORY:-0}" --maxmemory-policy "$${OKE_STORE_KV_MAXMEMORY_POLICY:-noeviction}"',
|
|
21
|
-
],
|
|
17
|
+
command: ["sh", "-c", kvServerCommand(s, "valkey-server")],
|
|
22
18
|
healthcheck: {
|
|
23
19
|
test: ["CMD", "valkey-cli", "-a", credEnv(s, "PASSWORD"), "ping"],
|
|
24
20
|
interval: "5s",
|
|
@@ -29,6 +29,10 @@ export async function openAnthropic(options: AiOpenOptions = {}): Promise<AiMode
|
|
|
29
29
|
const model = options.model ?? "claude-sonnet-4-20250514";
|
|
30
30
|
const baseUrl = (options.baseUrl ?? DEFAULT_BASE).replace(/\/$/, "");
|
|
31
31
|
const fetchFn = options.fetch ?? globalThis.fetch;
|
|
32
|
+
const preconnect = (fetchFn as { preconnect?: (href: string) => void }).preconnect;
|
|
33
|
+
if (typeof preconnect === "function") {
|
|
34
|
+
preconnect(baseUrl);
|
|
35
|
+
}
|
|
32
36
|
|
|
33
37
|
return {
|
|
34
38
|
driverId: "anthropic",
|
|
@@ -50,6 +54,7 @@ export async function openAnthropic(options: AiOpenOptions = {}): Promise<AiMode
|
|
|
50
54
|
...(system !== undefined ? { system } : {}),
|
|
51
55
|
messages,
|
|
52
56
|
}),
|
|
57
|
+
...(opts.signal !== undefined ? { signal: opts.signal } : {}),
|
|
53
58
|
});
|
|
54
59
|
const raw = (await res.json().catch(() => ({}))) as AnthropicMessagesResponse;
|
|
55
60
|
if (!res.ok) {
|
package/src/drivers/ai-ollama.ts
CHANGED
|
@@ -156,10 +156,7 @@ export async function openOllama(options: AiOpenOptions = {}): Promise<AiModelCl
|
|
|
156
156
|
const msg = raw.error ?? `ollama HTTP ${res.status}`;
|
|
157
157
|
throw new OllamaUnavailableError(`ollama: ${msg}`);
|
|
158
158
|
}
|
|
159
|
-
|
|
160
|
-
throw new OllamaUnavailableError("ollama: stream response has no body");
|
|
161
|
-
}
|
|
162
|
-
yield* readOllamaNdjson(res.body, opts.signal);
|
|
159
|
+
yield* readOllamaNdjson(res, opts.signal);
|
|
163
160
|
},
|
|
164
161
|
async embed(opts: AiEmbedOptions): Promise<AiEmbedResult> {
|
|
165
162
|
const resolvedModel = resolveOllamaModel(options, opts.model);
|
|
@@ -265,45 +262,45 @@ function parseOllamaToolCalls(
|
|
|
265
262
|
}
|
|
266
263
|
|
|
267
264
|
/**
|
|
268
|
-
* Parse Ollama NDJSON stream lines.
|
|
265
|
+
* Parse Ollama NDJSON stream lines via {@link Bun.JSONL.parseChunk}.
|
|
269
266
|
*
|
|
270
|
-
* @param
|
|
267
|
+
* @param res - Streaming response
|
|
271
268
|
* @param signal - Optional abort
|
|
272
269
|
*/
|
|
273
270
|
async function* readOllamaNdjson(
|
|
274
|
-
|
|
271
|
+
res: Response,
|
|
275
272
|
signal?: AbortSignal,
|
|
276
273
|
): AsyncGenerator<AiStreamChunk> {
|
|
277
|
-
const reader = body
|
|
274
|
+
const reader = res.body?.getReader();
|
|
275
|
+
if (!reader) {
|
|
276
|
+
throw new OllamaUnavailableError("ollama: stream response has no body");
|
|
277
|
+
}
|
|
278
278
|
const decoder = new TextDecoder();
|
|
279
|
-
let
|
|
279
|
+
let pending = "";
|
|
280
280
|
try {
|
|
281
281
|
while (true) {
|
|
282
|
-
if (signal?.aborted)
|
|
283
|
-
throw abortAsError(signal.reason);
|
|
284
|
-
}
|
|
282
|
+
if (signal?.aborted) throw abortAsError(signal.reason);
|
|
285
283
|
const { done, value } = await reader.read();
|
|
286
284
|
if (done) break;
|
|
287
|
-
|
|
288
|
-
const
|
|
289
|
-
|
|
290
|
-
for (const
|
|
291
|
-
const
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
if (chunk.done) {
|
|
300
|
-
yield { text: "", done: true };
|
|
301
|
-
return;
|
|
302
|
-
}
|
|
303
|
-
} catch {
|
|
304
|
-
// ignore malformed lines
|
|
285
|
+
pending += decoder.decode(value, { stream: true });
|
|
286
|
+
const parsed = bunJsonl().parseChunk(pending);
|
|
287
|
+
pending = pending.slice(parsed.read);
|
|
288
|
+
for (const row of parsed.values) {
|
|
289
|
+
const chunk = row as OllamaChatResponse;
|
|
290
|
+
const delta = chunk.message?.content;
|
|
291
|
+
if (typeof delta === "string" && delta.length > 0) {
|
|
292
|
+
yield { text: delta };
|
|
293
|
+
}
|
|
294
|
+
if (chunk.done) {
|
|
295
|
+
yield { text: "", done: true };
|
|
296
|
+
return;
|
|
305
297
|
}
|
|
306
298
|
}
|
|
299
|
+
if (parsed.error) {
|
|
300
|
+
const nl = pending.indexOf("\n");
|
|
301
|
+
if (nl === -1) continue;
|
|
302
|
+
pending = pending.slice(nl + 1);
|
|
303
|
+
}
|
|
307
304
|
}
|
|
308
305
|
yield { text: "", done: true };
|
|
309
306
|
} finally {
|
|
@@ -311,6 +308,28 @@ async function* readOllamaNdjson(
|
|
|
311
308
|
}
|
|
312
309
|
}
|
|
313
310
|
|
|
311
|
+
interface BunJsonlParse {
|
|
312
|
+
readonly values: readonly unknown[];
|
|
313
|
+
readonly read: number;
|
|
314
|
+
readonly done: boolean;
|
|
315
|
+
readonly error: unknown;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function bunJsonl(): {
|
|
319
|
+
parse(text: string): unknown[];
|
|
320
|
+
parseChunk(text: string): BunJsonlParse;
|
|
321
|
+
} {
|
|
322
|
+
const jsonl = (
|
|
323
|
+
Bun as typeof Bun & {
|
|
324
|
+
JSONL?: { parse: (t: string) => unknown[]; parseChunk: (t: string) => BunJsonlParse };
|
|
325
|
+
}
|
|
326
|
+
).JSONL;
|
|
327
|
+
if (!jsonl) {
|
|
328
|
+
throw new Error("ollama: Bun.JSONL is required (Bun >= 1.4.0)");
|
|
329
|
+
}
|
|
330
|
+
return jsonl;
|
|
331
|
+
}
|
|
332
|
+
|
|
314
333
|
/**
|
|
315
334
|
* Probe Ollama — fail loud before the first completion.
|
|
316
335
|
*
|
|
@@ -89,6 +89,7 @@ export async function openOpenaiCompatible(options: AiOpenOptions = {}): Promise
|
|
|
89
89
|
const model = options.model ?? "gpt-4o-mini";
|
|
90
90
|
const fetchFn = options.fetch ?? globalThis.fetch;
|
|
91
91
|
const extraHeaders = options.headers;
|
|
92
|
+
preconnectFetch(fetchFn, baseUrl);
|
|
92
93
|
|
|
93
94
|
return {
|
|
94
95
|
driverId: "openai-compatible",
|
|
@@ -162,10 +163,7 @@ export async function openOpenaiCompatible(options: AiOpenOptions = {}): Promise
|
|
|
162
163
|
const msg = raw.error?.message ?? `openai-compatible HTTP ${res.status}`;
|
|
163
164
|
throwHttp(`openai-compatible: ${msg}`, res.status);
|
|
164
165
|
}
|
|
165
|
-
|
|
166
|
-
throw new Error("openai-compatible: stream response has no body");
|
|
167
|
-
}
|
|
168
|
-
yield* readOpenaiSse(res.body, opts.signal);
|
|
166
|
+
yield* readOpenaiSse(res, opts.signal);
|
|
169
167
|
},
|
|
170
168
|
async embed(opts: AiEmbedOptions): Promise<AiEmbedResult> {
|
|
171
169
|
const resolvedModel = opts.model ?? model;
|
|
@@ -262,57 +260,70 @@ function parseToolCalls(
|
|
|
262
260
|
/**
|
|
263
261
|
* Parse OpenAI SSE chat.completion.chunk stream.
|
|
264
262
|
*
|
|
265
|
-
* @param
|
|
263
|
+
* @param res - Streaming response
|
|
266
264
|
* @param signal - Optional abort
|
|
267
265
|
*/
|
|
268
|
-
async function* readOpenaiSse(
|
|
269
|
-
body: ReadableStream<Uint8Array>,
|
|
270
|
-
signal?: AbortSignal,
|
|
271
|
-
): AsyncGenerator<AiStreamChunk> {
|
|
272
|
-
const reader = body.getReader();
|
|
273
|
-
const decoder = new TextDecoder();
|
|
266
|
+
async function* readOpenaiSse(res: Response, signal?: AbortSignal): AsyncGenerator<AiStreamChunk> {
|
|
274
267
|
let buffer = "";
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
268
|
+
for await (const piece of responseTextStream(res)) {
|
|
269
|
+
if (signal?.aborted) throw abortAsError(signal.reason);
|
|
270
|
+
buffer += piece;
|
|
271
|
+
const lines = buffer.split("\n");
|
|
272
|
+
buffer = lines.pop() ?? "";
|
|
273
|
+
for (const line of lines) {
|
|
274
|
+
const trimmed = line.trim();
|
|
275
|
+
if (!trimmed.startsWith("data:")) continue;
|
|
276
|
+
const data = trimmed.slice(5).trim();
|
|
277
|
+
if (data === "[DONE]") {
|
|
278
|
+
yield { text: "", done: true };
|
|
279
|
+
return;
|
|
279
280
|
}
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
281
|
+
try {
|
|
282
|
+
const chunk = JSON.parse(data) as {
|
|
283
|
+
choices?: readonly {
|
|
284
|
+
delta?: { content?: string | null };
|
|
285
|
+
finish_reason?: string | null;
|
|
286
|
+
}[];
|
|
287
|
+
};
|
|
288
|
+
const delta = chunk.choices?.[0]?.delta?.content;
|
|
289
|
+
if (typeof delta === "string" && delta.length > 0) {
|
|
290
|
+
yield { text: delta };
|
|
291
|
+
}
|
|
292
|
+
if (chunk.choices?.[0]?.finish_reason) {
|
|
290
293
|
yield { text: "", done: true };
|
|
291
294
|
return;
|
|
292
295
|
}
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
choices?: readonly {
|
|
296
|
-
delta?: { content?: string | null };
|
|
297
|
-
finish_reason?: string | null;
|
|
298
|
-
}[];
|
|
299
|
-
};
|
|
300
|
-
const delta = chunk.choices?.[0]?.delta?.content;
|
|
301
|
-
if (typeof delta === "string" && delta.length > 0) {
|
|
302
|
-
yield { text: delta };
|
|
303
|
-
}
|
|
304
|
-
if (chunk.choices?.[0]?.finish_reason) {
|
|
305
|
-
yield { text: "", done: true };
|
|
306
|
-
return;
|
|
307
|
-
}
|
|
308
|
-
} catch {
|
|
309
|
-
// ignore malformed SSE lines
|
|
310
|
-
}
|
|
296
|
+
} catch {
|
|
297
|
+
// ignore malformed SSE lines
|
|
311
298
|
}
|
|
312
299
|
}
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
300
|
+
}
|
|
301
|
+
yield { text: "", done: true };
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* UTF-8 text chunks from a Response (`textStream` on Bun ≥1.4).
|
|
306
|
+
*
|
|
307
|
+
* @param res - HTTP response
|
|
308
|
+
*/
|
|
309
|
+
function responseTextStream(res: Response): AsyncIterable<string> {
|
|
310
|
+
const stream = (res as Response & { textStream?: () => AsyncIterable<string> }).textStream;
|
|
311
|
+
if (typeof stream !== "function") {
|
|
312
|
+
throw new Error("openai-compatible: Response.textStream is required (Bun >= 1.4.0)");
|
|
313
|
+
}
|
|
314
|
+
return stream.call(res);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Warm DNS+TCP+TLS for a cloud origin. No-op when `fetch` is a test stub.
|
|
319
|
+
*
|
|
320
|
+
* @param fetchFn - Fetch implementation
|
|
321
|
+
* @param url - Provider base URL
|
|
322
|
+
*/
|
|
323
|
+
function preconnectFetch(fetchFn: typeof fetch, url: string): void {
|
|
324
|
+
const preconnect = (fetchFn as { preconnect?: (href: string) => void }).preconnect;
|
|
325
|
+
if (typeof preconnect === "function") {
|
|
326
|
+
preconnect(url);
|
|
316
327
|
}
|
|
317
328
|
}
|
|
318
329
|
|
|
@@ -43,12 +43,14 @@ describe("anthropic driver", () => {
|
|
|
43
43
|
model: "claude-test",
|
|
44
44
|
fetch: fetchFn,
|
|
45
45
|
});
|
|
46
|
+
const ac = new AbortController();
|
|
46
47
|
const result = await client.complete({
|
|
47
48
|
messages: [
|
|
48
49
|
{ role: "system", content: "be brief" },
|
|
49
50
|
{ role: "user", content: "hi" },
|
|
50
51
|
],
|
|
51
52
|
maxTokens: 64,
|
|
53
|
+
signal: ac.signal,
|
|
52
54
|
});
|
|
53
55
|
|
|
54
56
|
expect(result.driverId).toBe("anthropic");
|
|
@@ -62,6 +64,7 @@ describe("anthropic driver", () => {
|
|
|
62
64
|
};
|
|
63
65
|
expect(body.system).toBe("be brief");
|
|
64
66
|
expect(body.messages).toEqual([{ role: "user", content: "hi" } as { role: string }]);
|
|
67
|
+
expect(calls[0]!.init?.signal).toBe(ac.signal);
|
|
65
68
|
});
|
|
66
69
|
|
|
67
70
|
test("HTTP error surfaces provider message", async () => {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Bun native-client completeness — gaps closed vs platform limitations.
|
|
3
3
|
*
|
|
4
|
-
* Checked against Bun 1.
|
|
5
|
-
*
|
|
4
|
+
* Checked against Bun 1.4 (runtime). Do not invent custom protocol clients
|
|
5
|
+
* where Bun still lacks an API.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { describe, expect, test } from "bun:test";
|
|
@@ -46,7 +46,7 @@ export function bunNativeCompletenessReport(): readonly BunNativeGapRow[] {
|
|
|
46
46
|
status: hasTypedEval ? "closed" : "bun_limitation",
|
|
47
47
|
note: hasTypedEval
|
|
48
48
|
? "typed eval bound"
|
|
49
|
-
: "Bun
|
|
49
|
+
: "Bun.RedisClient.eval unavailable — would keep send(EVAL)",
|
|
50
50
|
},
|
|
51
51
|
{
|
|
52
52
|
surface: "redis signal",
|
|
@@ -63,7 +63,7 @@ export function bunNativeCompletenessReport(): readonly BunNativeGapRow[] {
|
|
|
63
63
|
status: hasTypedXadd ? "closed" : "bun_limitation",
|
|
64
64
|
note: hasTypedXadd
|
|
65
65
|
? "typed stream helpers bound"
|
|
66
|
-
: "Bun
|
|
66
|
+
: "Bun.RedisClient.xadd unavailable — Streams would keep send",
|
|
67
67
|
},
|
|
68
68
|
{
|
|
69
69
|
surface: "redis signal",
|
|
@@ -95,23 +95,21 @@ export function bunNativeCompletenessReport(): readonly BunNativeGapRow[] {
|
|
|
95
95
|
}
|
|
96
96
|
|
|
97
97
|
describe("Bun native client completeness", () => {
|
|
98
|
-
test("report table is exact and matches runtime Bun 1.
|
|
98
|
+
test("report table is exact and matches runtime Bun 1.4", () => {
|
|
99
99
|
const rows = bunNativeCompletenessReport();
|
|
100
100
|
expect(rows.length).toBe(8);
|
|
101
101
|
|
|
102
102
|
const closed = rows.filter((r) => r.status === "closed").map((r) => r.capability);
|
|
103
103
|
const limited = rows.filter((r) => r.status === "bun_limitation").map((r) => r.capability);
|
|
104
104
|
|
|
105
|
-
// Closed on Bun 1.3.14
|
|
106
105
|
expect(closed).toContain("SCAN (typed)");
|
|
106
|
+
expect(closed).toContain("EVAL (typed)");
|
|
107
107
|
expect(closed).toContain("native Bun bind (publish/subscribe)");
|
|
108
|
+
expect(closed).toContain("XADD / XREADGROUP (typed)");
|
|
108
109
|
expect(closed).toContain("send escape hatch");
|
|
109
110
|
expect(closed).toContain("Bun.SQL.unsafe query/exec");
|
|
110
111
|
expect(closed).toContain("Bun.S3Client file/list");
|
|
111
112
|
|
|
112
|
-
// Genuine Bun platform limitations (do not invent workarounds)
|
|
113
|
-
expect(limited).toContain("EVAL (typed)");
|
|
114
|
-
expect(limited).toContain("XADD / XREADGROUP (typed)");
|
|
115
113
|
expect(limited).toContain("LISTEN / NOTIFY on Bun.SQL");
|
|
116
114
|
});
|
|
117
115
|
|
package/src/drivers/redis.ts
CHANGED
|
@@ -140,11 +140,19 @@ function ttlToSeconds(ttl: string): number {
|
|
|
140
140
|
}
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
+
/** Bun 1.4 typed methods — `@types/bun` may lag the runtime. */
|
|
144
|
+
interface BunRedisEval {
|
|
145
|
+
eval(script: string, numkeys: number, ...keysAndArgs: string[]): Promise<unknown>;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function typedRedis(redis: Bun.RedisClient): Bun.RedisClient & BunRedisEval {
|
|
149
|
+
return redis as Bun.RedisClient & BunRedisEval;
|
|
150
|
+
}
|
|
151
|
+
|
|
143
152
|
/**
|
|
144
153
|
* Bind {@link Bun.RedisClient} / {@link Bun.redis} to {@link KvClientLike}.
|
|
145
154
|
*
|
|
146
|
-
* Uses typed `scan` (Bun ≥1.
|
|
147
|
-
* has no typed `eval` yet (platform limitation, not an OKE stub).
|
|
155
|
+
* Uses typed `scan` / `eval` (Bun ≥1.4). `send` remains the escape hatch.
|
|
148
156
|
*
|
|
149
157
|
* @param url - Optional Redis URL
|
|
150
158
|
*/
|
|
@@ -174,8 +182,7 @@ export function createBunRedisClient(url?: string): KvClientLike {
|
|
|
174
182
|
return redis.scan(cursor);
|
|
175
183
|
},
|
|
176
184
|
async eval(script, numkeys, ...keysAndArgs) {
|
|
177
|
-
|
|
178
|
-
return redis.send("EVAL", [script, String(numkeys), ...keysAndArgs]);
|
|
185
|
+
return typedRedis(redis).eval(script, numkeys, ...keysAndArgs);
|
|
179
186
|
},
|
|
180
187
|
send: (command, args) => redis.send(command, args),
|
|
181
188
|
};
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* exactly-once / fan-out physics stay correct under `drain()`.
|
|
8
8
|
*
|
|
9
9
|
* Production bind: {@link createBunSignalRedisClient} (typed `publish` /
|
|
10
|
-
* `subscribe
|
|
10
|
+
* `subscribe` / `xadd` / `xreadgroup` / `xack` / `xgroup` on Bun ≥1.4).
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { createSignalEngine } from "./signal-engine.ts";
|
|
@@ -18,12 +18,23 @@ import type {
|
|
|
18
18
|
SignalRedisClientLike,
|
|
19
19
|
} from "./signal-types.ts";
|
|
20
20
|
|
|
21
|
+
/** Bun 1.4 typed stream methods — `@types/bun` may lag the runtime. */
|
|
22
|
+
interface BunRedisStreams {
|
|
23
|
+
xadd(key: string, ...args: (string | number)[]): Promise<unknown>;
|
|
24
|
+
xreadgroup(...args: (string | number)[]): Promise<unknown>;
|
|
25
|
+
xack(key: string, group: string, id: string): Promise<unknown>;
|
|
26
|
+
xgroup(subcommand: string, ...args: (string | number)[]): Promise<unknown>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function typedStreams(redis: Bun.RedisClient): Bun.RedisClient & BunRedisStreams {
|
|
30
|
+
return redis as Bun.RedisClient & BunRedisStreams;
|
|
31
|
+
}
|
|
32
|
+
|
|
21
33
|
/**
|
|
22
34
|
* Bind {@link Bun.RedisClient} to {@link SignalRedisClientLike}.
|
|
23
35
|
*
|
|
24
|
-
* Streams
|
|
25
|
-
*
|
|
26
|
-
* Pub/sub uses typed `publish` / `subscribe` (gap closed vs earlier fake-only).
|
|
36
|
+
* Streams use typed `xadd` / `xgroup` / `xreadgroup` / `xack` (Bun ≥1.4).
|
|
37
|
+
* Pub/sub uses typed `publish` / `subscribe`.
|
|
27
38
|
*
|
|
28
39
|
* @param url - Optional Redis URL
|
|
29
40
|
*/
|
|
@@ -41,37 +52,36 @@ export function createBunSignalRedisClient(url?: string): SignalRedisClientLike
|
|
|
41
52
|
|
|
42
53
|
return {
|
|
43
54
|
async xadd(key, id, fields) {
|
|
44
|
-
const
|
|
55
|
+
const pairs: string[] = [];
|
|
45
56
|
for (const [k, v] of Object.entries(fields)) {
|
|
46
|
-
|
|
57
|
+
pairs.push(k, v);
|
|
47
58
|
}
|
|
48
|
-
return String(await redis.
|
|
59
|
+
return String(await typedStreams(redis).xadd(key, id, ...pairs));
|
|
49
60
|
},
|
|
50
61
|
async xgroupCreate(key, group, id, opts) {
|
|
51
|
-
const
|
|
52
|
-
if (opts?.mkstream) args.push("MKSTREAM");
|
|
62
|
+
const extra = opts?.mkstream ? (["MKSTREAM"] as const) : [];
|
|
53
63
|
try {
|
|
54
|
-
await redis.
|
|
64
|
+
await typedStreams(redis).xgroup("CREATE", key, group, id, ...extra);
|
|
55
65
|
} catch (err) {
|
|
56
66
|
const msg = err instanceof Error ? err.message : String(err);
|
|
57
67
|
if (!/BUSYGROUP/i.test(msg)) throw err;
|
|
58
68
|
}
|
|
59
69
|
},
|
|
60
70
|
async xreadgroup(group, consumer, key, count) {
|
|
61
|
-
const reply = await redis.
|
|
71
|
+
const reply = await typedStreams(redis).xreadgroup(
|
|
62
72
|
"GROUP",
|
|
63
73
|
group,
|
|
64
74
|
consumer,
|
|
65
75
|
"COUNT",
|
|
66
|
-
|
|
76
|
+
count,
|
|
67
77
|
"STREAMS",
|
|
68
78
|
key,
|
|
69
79
|
">",
|
|
70
|
-
|
|
80
|
+
);
|
|
71
81
|
return parseXreadgroupReply(reply);
|
|
72
82
|
},
|
|
73
83
|
async xack(key, group, id) {
|
|
74
|
-
return Number(await redis.
|
|
84
|
+
return Number(await typedStreams(redis).xack(key, group, id));
|
|
75
85
|
},
|
|
76
86
|
async publish(channel, message) {
|
|
77
87
|
return redis.publish(channel, message);
|
|
@@ -69,8 +69,9 @@ export interface SignalMessage {
|
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
/** Dead-letter entry with full attempt history. */
|
|
72
|
-
export interface DeadLetter extends SignalMessage {
|
|
72
|
+
export interface DeadLetter<T = unknown> extends Omit<SignalMessage, "payload" | "status"> {
|
|
73
73
|
readonly status: "dead";
|
|
74
|
+
readonly payload: T;
|
|
74
75
|
}
|
|
75
76
|
|
|
76
77
|
/** Per-subscriber lag / errors (broadcast physics). */
|
package/src/drivers/types.ts
CHANGED
|
@@ -83,7 +83,7 @@ export interface SqlDriver {
|
|
|
83
83
|
/** Key-value namespace handle. */
|
|
84
84
|
export interface KvNamespace {
|
|
85
85
|
/** Protocol driver id. */
|
|
86
|
-
readonly driverId: "memory" | "redis";
|
|
86
|
+
readonly driverId: "memory" | "redis" | "postgres" | "pglite";
|
|
87
87
|
/**
|
|
88
88
|
* Read a key.
|
|
89
89
|
*
|