okengine 0.9.0 → 0.10.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/AGENTS.md +1 -1
- package/manifest.v1.schema.json +9 -1
- package/package.json +3 -3
- package/site/content/docs/console/runs.mdx +10 -0
- package/site/content/docs/console/traces.mdx +4 -0
- package/site/content/docs/elements/ai.mdx +54 -32
- package/site/content/docs/elements/channel.mdx +105 -14
- package/site/content/docs/elements/clock.mdx +1 -0
- package/site/content/docs/elements/flow.mdx +42 -8
- package/site/content/docs/get-started/basic-usage.mdx +5 -1
- package/site/content/docs/get-started/installation.mdx +9 -5
- package/site/content/docs/meta.json +2 -0
- package/site/content/docs/plugins/otp.mdx +58 -46
- package/site/content/docs/providers/azure-redis.mdx +85 -0
- package/site/content/docs/providers/cockroachdb.mdx +86 -0
- package/site/content/docs/providers/digitalocean-caching.mdx +88 -0
- package/site/content/docs/providers/dragonfly-cloud.mdx +82 -0
- package/site/content/docs/providers/elasticache.mdx +92 -0
- package/site/content/docs/providers/index.mdx +101 -0
- package/site/content/docs/providers/memorystore.mdx +89 -0
- package/site/content/docs/providers/meta.json +20 -0
- package/site/content/docs/providers/neon.mdx +115 -0
- package/site/content/docs/providers/redis-cloud.mdx +93 -0
- package/site/content/docs/providers/supabase.mdx +117 -0
- package/site/content/docs/providers/upstash.mdx +91 -0
- package/site/content/docs/providers/yugabytedb.mdx +95 -0
- package/site/content/docs/recipes/caddy.mdx +128 -0
- package/site/content/docs/recipes/cockroachdb.mdx +127 -0
- package/site/content/docs/recipes/dragonfly.mdx +125 -0
- package/site/content/docs/recipes/index.mdx +146 -0
- package/site/content/docs/recipes/llama-cpp.mdx +153 -0
- package/site/content/docs/recipes/mailpit.mdx +136 -0
- package/site/content/docs/recipes/meilisearch.mdx +139 -0
- package/site/content/docs/recipes/meta.json +31 -0
- package/site/content/docs/recipes/ollama.mdx +142 -0
- package/site/content/docs/recipes/openbao.mdx +141 -0
- package/site/content/docs/recipes/pgdog.mdx +137 -0
- package/site/content/docs/recipes/postgres.mdx +136 -0
- package/site/content/docs/recipes/redis.mdx +130 -0
- package/site/content/docs/recipes/rustfs.mdx +148 -0
- package/site/content/docs/recipes/sglang.mdx +105 -0
- package/site/content/docs/recipes/supabase-docker.mdx +145 -0
- package/site/content/docs/recipes/timescale.mdx +129 -0
- package/site/content/docs/recipes/traefik.mdx +136 -0
- package/site/content/docs/recipes/valkey.mdx +121 -0
- package/site/content/docs/recipes/vllm.mdx +106 -0
- package/site/content/docs/recipes/yugabytedb.mdx +135 -0
- package/site/content/docs/reference/cli.md +42 -6
- package/site/content/docs/reference/configuration.mdx +5 -3
- package/site/content/docs/reference/environment-variables.mdx +12 -12
- package/site/content/docs/reference/fx.mdx +38 -4
- package/src/auth/otp-capability.ts +14 -14
- package/src/cli/ai-setup/ai-setup.test.ts +32 -12
- package/src/cli/ai-setup/apply.ts +65 -23
- package/src/cli/ai-setup/catalog.ts +1316 -35
- package/src/cli/ai-setup/detect-ollama.ts +47 -0
- package/src/cli/ai-setup/index.ts +44 -7
- package/src/cli/ai-setup/prompts.ts +430 -486
- package/src/cli/ai-setup/recommend.test.ts +150 -50
- package/src/cli/ai-setup/recommend.ts +118 -101
- package/src/cli/ask-seed.test.ts +68 -0
- package/src/cli/ask-seed.ts +79 -0
- package/src/cli/db-seed-redact.test.ts +25 -0
- package/src/cli/db-seed.ts +36 -7
- package/src/cli/db.test.ts +1 -1
- package/src/cli/db.ts +2 -2
- package/src/cli/dev-controls.test.ts +107 -0
- package/src/cli/dev-controls.ts +354 -0
- package/src/cli/dev-schema-sync.ts +12 -4
- package/src/cli/dev.test.ts +50 -30
- package/src/cli/dev.ts +607 -66
- package/src/cli/drizzle-env.test.ts +40 -6
- package/src/cli/drizzle-env.ts +22 -1
- package/src/cli/ensure-drizzle-config.ts +6 -4
- package/src/cli/hero-meta.test.ts +13 -1
- package/src/cli/hero-meta.ts +28 -6
- package/src/cli/index.ts +5 -0
- package/src/cli/registry.ts +24 -0
- package/src/cli/replay.test.ts +105 -0
- package/src/cli/replay.ts +206 -0
- package/src/compiler/extract.ts +3 -3
- package/src/config/index.ts +3 -3
- package/src/console/server/claim.ts +3 -1
- package/src/console/ui/dist/assets/{index-ClX0ZCe5.js → index-D-9KFce5.js} +2 -2
- package/src/console/ui/dist/assets/panel-overview-CW0CQEc2.js +1 -0
- package/src/console/ui/dist/assets/panel-runs-BxuyDI3x.js +1 -0
- package/src/console/ui/dist/assets/{panel-signals-Dsxdu_AR.js → panel-signals-BXofbj7v.js} +1 -1
- package/src/console/ui/dist/assets/{panel-store-hy7O8HOs.js → panel-store-B8mH-arU.js} +1 -1
- package/src/console/ui/dist/assets/{panel-traces-CBiAe5go.js → panel-traces-DSA-ZWnf.js} +1 -1
- package/src/console/ui/dist/index.html +1 -1
- package/src/console/ui/overview/slo.ts +31 -3
- package/src/console/ui/overview/types.ts +6 -0
- package/src/console/ui/runs/errors.test.ts +53 -0
- package/src/console/ui/runs/errors.ts +88 -0
- package/src/console/ui/runs/index.ts +8 -0
- package/src/console/ui/runs/search.ts +16 -0
- package/src/console/ui/shell/panels/runs/RunsPanel.tsx +72 -1
- package/src/docker/ai-model-status.test.ts +101 -0
- package/src/docker/ai-model-status.ts +276 -0
- package/src/docker/compose-health.test.ts +115 -0
- package/src/docker/compose-health.ts +234 -0
- package/src/docker/compose.ts +24 -8
- package/src/docker/derive.ts +35 -9
- package/src/docker/docker.test.ts +304 -12
- package/src/docker/helpers.ts +56 -0
- package/src/docker/index.ts +52 -0
- package/src/docker/ollama-pull.ts +232 -0
- package/src/docker/recipes/cockroach.ts +24 -0
- package/src/docker/recipes/index.ts +32 -0
- package/src/docker/recipes/llama-cpp.ts +273 -0
- package/src/docker/recipes/ollama.ts +19 -23
- package/src/docker/recipes/pgdog.ts +4 -1
- package/src/docker/recipes/postgres.ts +5 -18
- package/src/docker/recipes/sglang.ts +55 -0
- package/src/docker/recipes/supabase.ts +21 -0
- package/src/docker/recipes/timescale.ts +18 -0
- package/src/docker/recipes/vllm.ts +44 -0
- package/src/docker/recipes/yugabyte.ts +23 -0
- package/src/docker/stack-id.test.ts +22 -0
- package/src/docker/stack-id.ts +38 -4
- package/src/docker/types.ts +15 -0
- package/src/drivers/ai-ollama.integration.test.ts +7 -4
- package/src/drivers/signal-engine.ts +10 -0
- package/src/drivers/signal-postgres.ts +24 -2
- package/src/drivers/signal-types.ts +10 -0
- package/src/elements/channel/fallback.test.ts +91 -0
- package/src/elements/channel/hard-bounce.test.ts +82 -0
- package/src/elements/channel/injection.test.ts +153 -0
- package/src/elements/channel/locale-ar.test.ts +102 -0
- package/src/elements/channel/otp-delivery.ts +5 -2
- package/src/elements/channel/receipts-ledger.test.ts +100 -0
- package/src/elements/channel/runtime.ts +24 -1
- package/src/elements/channel/test-helpers.ts +60 -0
- package/src/elements/channel/whatsapp-compliance.test.ts +124 -0
- package/src/elements/store/cache.ts +1 -1
- package/src/elements/store/emit-drizzle.ts +43 -9
- package/src/elements/store/runtime.ts +1 -1
- package/src/elements/store/schema-decl.test.ts +2 -0
- package/src/elements/store/schema-decl.ts +15 -0
- package/src/elements/store/seed.ts +1 -1
- package/src/index.ts +1 -0
- package/src/kernel/app.ts +118 -11
- package/src/kernel/boot-bind/signal.ts +6 -2
- package/src/kernel/boot-bind/store.ts +2 -1
- package/src/kernel/boot.test.ts +6 -5
- package/src/kernel/boot.ts +8 -3
- package/src/kernel/compensate.test.ts +131 -0
- package/src/kernel/correlation.test.ts +108 -0
- package/src/kernel/effect-timing.test.ts +82 -0
- package/src/kernel/flow.ts +38 -0
- package/src/kernel/fx-runs.test.ts +49 -0
- package/src/kernel/fx.test.ts +14 -0
- package/src/kernel/fx.ts +111 -1
- package/src/manifest/types.ts +8 -2
- package/src/plugins/auth-delivery.mailpit.integration.test.ts +1 -1
- package/src/plugins/auth-methods.security.test.ts +3 -3
- package/src/plugins/auth-methods.test.ts +1 -1
- package/src/plugins/index.ts +2 -2
- package/src/plugins/otp.test.ts +21 -19
- package/src/plugins/otp.ts +36 -34
- package/src/plugins/taqnyat.live.test.ts +3 -3
- package/src/runs/collect.ts +3 -0
- package/src/runs/export-otlp.ts +64 -0
- package/src/runs/index.ts +16 -0
- package/src/runs/parquet.ts +9 -0
- package/src/runs/types.ts +5 -0
- package/src/runs/window.test.ts +68 -0
- package/src/runs/window.ts +148 -0
- package/src/runtime/boot-warn.test.ts +45 -0
- package/src/runtime/boot-warn.ts +17 -0
- package/src/runtime/dev-request-log.ts +5 -5
- package/src/term.test.ts +95 -10
- package/src/term.ts +369 -14
- package/src/console/ui/dist/assets/panel-overview-DONDzBd2.js +0 -1
- package/src/console/ui/dist/assets/panel-runs-D5zf-D9c.js +0 -1
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
buildPgDogUsersToml,
|
|
16
16
|
builtinRecipes,
|
|
17
17
|
caddy,
|
|
18
|
+
cockroach,
|
|
18
19
|
deriveInfrastructure,
|
|
19
20
|
dragonfly,
|
|
20
21
|
emitDockerfile,
|
|
@@ -26,11 +27,25 @@ import {
|
|
|
26
27
|
resolveStack,
|
|
27
28
|
SOCKET_PROXY_IMAGE,
|
|
28
29
|
SOCKET_PROXY_SERVICE,
|
|
30
|
+
timescale,
|
|
29
31
|
traefik,
|
|
30
32
|
traefikAppLabels,
|
|
31
33
|
valkey,
|
|
32
34
|
writeDerivedFiles,
|
|
35
|
+
yugabyte,
|
|
36
|
+
ensureOllamaModel,
|
|
37
|
+
llamaCpp,
|
|
38
|
+
LLAMA_CPP_IMAGE,
|
|
39
|
+
LLAMA_CPP_MIN_SAFE_BUILD,
|
|
40
|
+
OllamaPullError,
|
|
41
|
+
OLLAMA_IMAGE,
|
|
42
|
+
OLLAMA_MIN_SAFE_VERSION,
|
|
43
|
+
sglang,
|
|
44
|
+
SGLANG_IMAGE,
|
|
45
|
+
vllm,
|
|
46
|
+
VLLM_IMAGE,
|
|
33
47
|
type ImageRecipe,
|
|
48
|
+
type OllamaFetch,
|
|
34
49
|
type ServiceSpec,
|
|
35
50
|
} from "./index.ts";
|
|
36
51
|
|
|
@@ -152,6 +167,95 @@ describe("image recipes", () => {
|
|
|
152
167
|
expect(derived.stackEnv.REDIS_URL).toContain("redis://");
|
|
153
168
|
});
|
|
154
169
|
|
|
170
|
+
test("cockroach matches official image, port 26257, COCKROACH_* env, sslmode=require URL", () => {
|
|
171
|
+
const image = "cockroachdb/cockroach:v25.2.0";
|
|
172
|
+
expect(recipeFor(image).id).toBe("cockroach");
|
|
173
|
+
expect(cockroach.match(image)).toBe(true);
|
|
174
|
+
expect(postgres.match(image)).toBe(false);
|
|
175
|
+
const spec: ServiceSpec = {
|
|
176
|
+
role: "store.sql",
|
|
177
|
+
serviceName: "store-sql",
|
|
178
|
+
image,
|
|
179
|
+
port: 26257,
|
|
180
|
+
hostPort: 5432,
|
|
181
|
+
credentials: fixedCreds["store.sql"],
|
|
182
|
+
};
|
|
183
|
+
const applied = recipeFor(spec.image).apply(spec);
|
|
184
|
+
expect(applied.command).toEqual(["start-single-node", "--accept-sql-without-tls"]);
|
|
185
|
+
expect(applied.environment?.COCKROACH_USER).toBe("${OKE_STORE_SQL_USER}");
|
|
186
|
+
expect(applied.environment?.COCKROACH_PASSWORD).toBe("${OKE_STORE_SQL_PASSWORD}");
|
|
187
|
+
expect(applied.environment?.COCKROACH_DATABASE).toBe("${OKE_STORE_SQL_DB}");
|
|
188
|
+
expect(applied.extraPorts).toEqual([{ host: 8080, container: 8080 }]);
|
|
189
|
+
expect(applied.volumes?.[0]).toContain("/cockroach/cockroach-data");
|
|
190
|
+
const url = recipeFor(spec.image).url(spec, {
|
|
191
|
+
host: "127.0.0.1",
|
|
192
|
+
port: 5432,
|
|
193
|
+
...fixedCreds["store.sql"],
|
|
194
|
+
});
|
|
195
|
+
expect(url).toContain("sslmode=require");
|
|
196
|
+
expect(url).toContain(":5432/");
|
|
197
|
+
|
|
198
|
+
const derived = deriveInfrastructure({
|
|
199
|
+
images: { "store.sql": image },
|
|
200
|
+
credentials: { "store.sql": fixedCreds["store.sql"] },
|
|
201
|
+
});
|
|
202
|
+
expect(derived.specs[0]!.port).toBe(26257);
|
|
203
|
+
expect(derived.specs[0]!.hostPort).toBe(5432);
|
|
204
|
+
const sqlYml = derived.files.find((f) => f.path === "compose.store.sql.yml")?.content ?? "";
|
|
205
|
+
expect(sqlYml).toContain("26257");
|
|
206
|
+
expect(sqlYml).toContain("COCKROACH_");
|
|
207
|
+
expect(derived.stackEnv.DATABASE_URL).toContain("sslmode=require");
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test("yugabyte matches official image, YSQL on 5433, YSQL_* env", () => {
|
|
211
|
+
const image = "yugabytedb/yugabyte:2025.1.0.0-b100";
|
|
212
|
+
expect(recipeFor(image).id).toBe("yugabyte");
|
|
213
|
+
expect(yugabyte.match(image)).toBe(true);
|
|
214
|
+
const spec: ServiceSpec = {
|
|
215
|
+
role: "store.sql",
|
|
216
|
+
serviceName: "store-sql",
|
|
217
|
+
image,
|
|
218
|
+
port: 5433,
|
|
219
|
+
hostPort: 5432,
|
|
220
|
+
credentials: fixedCreds["store.sql"],
|
|
221
|
+
};
|
|
222
|
+
const applied = recipeFor(spec.image).apply(spec);
|
|
223
|
+
expect(String(applied.command)).toContain("yugabyted");
|
|
224
|
+
expect(applied.environment?.YSQL_USER).toBe("${OKE_STORE_SQL_USER}");
|
|
225
|
+
expect(applied.environment?.YSQL_PASSWORD).toBe("${OKE_STORE_SQL_PASSWORD}");
|
|
226
|
+
expect(applied.environment?.YSQL_DB).toBe("${OKE_STORE_SQL_DB}");
|
|
227
|
+
expect(applied.volumes?.[0]).toContain("/home/yugabyte/yb_data");
|
|
228
|
+
const url = recipeFor(spec.image).url(spec, {
|
|
229
|
+
host: "127.0.0.1",
|
|
230
|
+
port: 5432,
|
|
231
|
+
...fixedCreds["store.sql"],
|
|
232
|
+
});
|
|
233
|
+
expect(url).toStartWith("postgres://oke:");
|
|
234
|
+
|
|
235
|
+
const derived = deriveInfrastructure({
|
|
236
|
+
images: { "store.sql": image },
|
|
237
|
+
credentials: { "store.sql": fixedCreds["store.sql"] },
|
|
238
|
+
});
|
|
239
|
+
expect(derived.specs[0]!.port).toBe(5433);
|
|
240
|
+
const sqlYml = derived.files.find((f) => f.path === "compose.store.sql.yml")?.content ?? "";
|
|
241
|
+
expect(sqlYml).toContain("5433");
|
|
242
|
+
expect(sqlYml).toContain("YSQL_");
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test("timescale matches ahead of generic postgres with the same POSTGRES_* contract", () => {
|
|
246
|
+
const image = "timescale/timescaledb:latest-pg17";
|
|
247
|
+
expect(recipeFor(image).id).toBe("timescale");
|
|
248
|
+
expect(timescale.match(image)).toBe(true);
|
|
249
|
+
expect(postgres.match(image)).toBe(false);
|
|
250
|
+
const derived = deriveInfrastructure({
|
|
251
|
+
images: { "store.sql": image },
|
|
252
|
+
credentials: { "store.sql": fixedCreds["store.sql"] },
|
|
253
|
+
});
|
|
254
|
+
const sqlYml = derived.files.find((f) => f.path === "compose.store.sql.yml")?.content ?? "";
|
|
255
|
+
expect(sqlYml).toContain("POSTGRES_USER");
|
|
256
|
+
expect(derived.stackEnv.DATABASE_URL).toContain("postgres://");
|
|
257
|
+
});
|
|
258
|
+
|
|
155
259
|
test("pgdog matches the official image and waits on store-sql", () => {
|
|
156
260
|
expect(recipeFor("ghcr.io/pgdogdev/pgdog:v0.1.51").id).toBe("pgdog");
|
|
157
261
|
expect(pgdog.match("ghcr.io/pgdogdev/pgdog:main")).toBe(true);
|
|
@@ -215,25 +319,27 @@ describe("image recipes", () => {
|
|
|
215
319
|
expect(url).toBe("http://127.0.0.1:7700");
|
|
216
320
|
});
|
|
217
321
|
|
|
218
|
-
test("ollama matches the official image,
|
|
219
|
-
expect(recipeFor(
|
|
322
|
+
test("ollama matches the official image, serves on 11434, emits http URL", () => {
|
|
323
|
+
expect(recipeFor(OLLAMA_IMAGE).id).toBe("ollama");
|
|
324
|
+
expect(OLLAMA_IMAGE).not.toContain("latest");
|
|
325
|
+
expect(OLLAMA_MIN_SAFE_VERSION).toBe("0.17.1");
|
|
220
326
|
const spec: ServiceSpec = {
|
|
221
327
|
role: "ai",
|
|
222
328
|
serviceName: "ai",
|
|
223
|
-
image:
|
|
329
|
+
image: OLLAMA_IMAGE,
|
|
224
330
|
port: 11434,
|
|
225
331
|
hostPort: 11434,
|
|
226
332
|
credentials: { user: "oke", password: "unused", database: "oke" },
|
|
227
333
|
};
|
|
228
334
|
const applied = recipeFor(spec.image).apply(spec);
|
|
229
335
|
expect(applied.environment?.OKE_AI_MODEL).toBe("${OKE_AI_MODEL:-qwen3.5:9b}");
|
|
336
|
+
expect(applied.environment?.OLLAMA_HOST).toBe("0.0.0.0:11434");
|
|
337
|
+
expect(applied.publishBind).toBe("127.0.0.1");
|
|
230
338
|
expect(applied.volumes).toContain("ai-data:/root/.ollama");
|
|
231
339
|
expect(applied.healthcheck?.test.join(" ")).toContain("ollama list");
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
expect(
|
|
235
|
-
expect(String(applied.command)).toContain("wait $$pid");
|
|
236
|
-
expect(String(applied.command)).not.toMatch(/(?<!\$)\$pid\b/);
|
|
340
|
+
// Pull is host-side via ensureOllamaModel — never a boot `ollama pull` CLI.
|
|
341
|
+
expect(applied.command).toBeUndefined();
|
|
342
|
+
expect(applied.entrypoint).toBeUndefined();
|
|
237
343
|
const url = recipeFor(spec.image).url(spec, {
|
|
238
344
|
host: "127.0.0.1",
|
|
239
345
|
port: 11434,
|
|
@@ -244,6 +350,153 @@ describe("image recipes", () => {
|
|
|
244
350
|
expect(url).toBe("http://127.0.0.1:11434");
|
|
245
351
|
});
|
|
246
352
|
|
|
353
|
+
test("llama-cpp is the default local AI recipe — OpenAI /v1, loopback publish, pinned ≥ b8146", () => {
|
|
354
|
+
expect(recipeFor(LLAMA_CPP_IMAGE).id).toBe("llama-cpp");
|
|
355
|
+
expect(LLAMA_CPP_IMAGE).not.toContain("latest");
|
|
356
|
+
const build = Number(/server-b(\d+)/.exec(LLAMA_CPP_IMAGE)?.[1]);
|
|
357
|
+
expect(build).toBeGreaterThanOrEqual(LLAMA_CPP_MIN_SAFE_BUILD);
|
|
358
|
+
const spec: ServiceSpec = {
|
|
359
|
+
role: "ai",
|
|
360
|
+
serviceName: "ai",
|
|
361
|
+
image: LLAMA_CPP_IMAGE,
|
|
362
|
+
port: 8080,
|
|
363
|
+
hostPort: 8080,
|
|
364
|
+
credentials: { user: "oke", password: "unused", database: "oke" },
|
|
365
|
+
};
|
|
366
|
+
const applied = llamaCpp.apply(spec);
|
|
367
|
+
expect(applied.publishBind).toBe("127.0.0.1");
|
|
368
|
+
// Never put docker-repo / models-preset / interpolated OKE_AI_MODEL in
|
|
369
|
+
// service environment — router children recurse / OOM; Compose ${} would
|
|
370
|
+
// also override env_file from an empty host shell.
|
|
371
|
+
expect(applied.environment?.LLAMA_ARG_MODELS_PRESET).toBeUndefined();
|
|
372
|
+
expect(applied.environment?.LLAMA_ARG_DOCKER_REPO).toBeUndefined();
|
|
373
|
+
expect(applied.environment?.OKE_AI_MODEL).toBeUndefined();
|
|
374
|
+
expect(applied.entrypoint).toEqual(["/usr/bin/python3", "/oke/llama-entrypoint.py"]);
|
|
375
|
+
expect(applied.command).toBeUndefined();
|
|
376
|
+
expect(applied.volumes?.some((v) => v.includes("llama-entrypoint.py"))).toBe(true);
|
|
377
|
+
expect(applied.extraPorts).toBeUndefined();
|
|
378
|
+
expect(llamaCpp.url(spec, { host: "127.0.0.1", port: 8080, ...spec.credentials })).toBe(
|
|
379
|
+
"http://127.0.0.1:8080/v1",
|
|
380
|
+
);
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
test("vllm recipe is GPU-aware, OpenAI /v1, loopback publish, never latest", () => {
|
|
384
|
+
expect(recipeFor(VLLM_IMAGE).id).toBe("vllm");
|
|
385
|
+
expect(VLLM_IMAGE).not.toContain("latest");
|
|
386
|
+
const spec: ServiceSpec = {
|
|
387
|
+
role: "ai",
|
|
388
|
+
serviceName: "ai",
|
|
389
|
+
image: VLLM_IMAGE,
|
|
390
|
+
port: 8000,
|
|
391
|
+
hostPort: 8000,
|
|
392
|
+
credentials: { user: "oke", password: "unused", database: "oke" },
|
|
393
|
+
};
|
|
394
|
+
const applied = vllm.apply(spec);
|
|
395
|
+
expect(applied.publishBind).toBe("127.0.0.1");
|
|
396
|
+
expect(applied.ipc).toBe("host");
|
|
397
|
+
expect(JSON.stringify(applied.deploy)).toContain("nvidia");
|
|
398
|
+
expect(vllm.url(spec, { host: "127.0.0.1", port: 8000, ...spec.credentials })).toBe(
|
|
399
|
+
"http://127.0.0.1:8000/v1",
|
|
400
|
+
);
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
test("sglang recipe is GPU-aware, OpenAI /v1, loopback publish, never latest", () => {
|
|
404
|
+
expect(recipeFor(SGLANG_IMAGE).id).toBe("sglang");
|
|
405
|
+
expect(SGLANG_IMAGE).not.toContain("latest");
|
|
406
|
+
const spec: ServiceSpec = {
|
|
407
|
+
role: "ai",
|
|
408
|
+
serviceName: "ai",
|
|
409
|
+
image: SGLANG_IMAGE,
|
|
410
|
+
port: 30000,
|
|
411
|
+
hostPort: 30000,
|
|
412
|
+
credentials: { user: "oke", password: "unused", database: "oke" },
|
|
413
|
+
};
|
|
414
|
+
const applied = sglang.apply(spec);
|
|
415
|
+
expect(applied.publishBind).toBe("127.0.0.1");
|
|
416
|
+
expect(applied.ipc).toBe("host");
|
|
417
|
+
expect(JSON.stringify(applied.deploy)).toContain("nvidia");
|
|
418
|
+
expect(sglang.url(spec, { host: "127.0.0.1", port: 30000, ...spec.credentials })).toBe(
|
|
419
|
+
"http://127.0.0.1:30000/v1",
|
|
420
|
+
);
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
test("ensureOllamaModel POSTs /api/pull to the container base URL (not a host CLI)", async () => {
|
|
424
|
+
const calls: { url: string; method: string; body?: string }[] = [];
|
|
425
|
+
const fetchFn: OllamaFetch = async (input, init) => {
|
|
426
|
+
const url = String(input);
|
|
427
|
+
const method = (init?.method ?? "GET").toUpperCase();
|
|
428
|
+
const body = typeof init?.body === "string" ? init.body : undefined;
|
|
429
|
+
calls.push({ url, method, ...(body !== undefined ? { body } : {}) });
|
|
430
|
+
if (url.endsWith("/api/tags") && method === "GET") {
|
|
431
|
+
return new Response(JSON.stringify({ models: [] }), { status: 200 });
|
|
432
|
+
}
|
|
433
|
+
if (url.endsWith("/api/pull") && method === "POST") {
|
|
434
|
+
expect(body).toContain('"model":"qwen3.5:9b"');
|
|
435
|
+
expect(body).toContain('"stream":true');
|
|
436
|
+
return new Response(`${JSON.stringify({ status: "success" })}\n`, { status: 200 });
|
|
437
|
+
}
|
|
438
|
+
return new Response("unexpected", { status: 500 });
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
await ensureOllamaModel({
|
|
442
|
+
url: "http://127.0.0.1:11434",
|
|
443
|
+
model: "qwen3.5:9b",
|
|
444
|
+
fetch: fetchFn,
|
|
445
|
+
readyTimeoutMs: 2_000,
|
|
446
|
+
pullTimeoutMs: 5_000,
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
expect(
|
|
450
|
+
calls.some((c) => c.url === "http://127.0.0.1:11434/api/tags" && c.method === "GET"),
|
|
451
|
+
).toBe(true);
|
|
452
|
+
const pull = calls.find((c) => c.url === "http://127.0.0.1:11434/api/pull");
|
|
453
|
+
expect(pull?.method).toBe("POST");
|
|
454
|
+
expect(pull?.body).toBe(JSON.stringify({ model: "qwen3.5:9b", stream: true }));
|
|
455
|
+
// No host-side `ollama` binary assumption — only HTTP to the given URL.
|
|
456
|
+
expect(calls.every((c) => c.url.startsWith("http://127.0.0.1:11434/"))).toBe(true);
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
test("ensureOllamaModel skips pull when /api/tags already lists the model", async () => {
|
|
460
|
+
const calls: string[] = [];
|
|
461
|
+
const statuses: string[] = [];
|
|
462
|
+
const fetchFn: OllamaFetch = async (input, init) => {
|
|
463
|
+
const url = String(input);
|
|
464
|
+
const method = (init?.method ?? "GET").toUpperCase();
|
|
465
|
+
calls.push(`${method} ${url}`);
|
|
466
|
+
if (url.endsWith("/api/tags")) {
|
|
467
|
+
return new Response(JSON.stringify({ models: [{ name: "gemma4:e4b" }] }), {
|
|
468
|
+
status: 200,
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
return new Response("should not pull", { status: 500 });
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
await ensureOllamaModel({
|
|
475
|
+
url: "http://127.0.0.1:23100",
|
|
476
|
+
model: "gemma4:e4b",
|
|
477
|
+
fetch: fetchFn,
|
|
478
|
+
readyTimeoutMs: 2_000,
|
|
479
|
+
onStatus: (line) => statuses.push(line),
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
expect(calls.some((c) => c.includes("/api/pull"))).toBe(false);
|
|
483
|
+
expect(statuses.some((s) => /already has gemma4:e4b/.test(s))).toBe(true);
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
test("ensureOllamaModel fails loud when the container API never becomes ready", async () => {
|
|
487
|
+
const fetchFn: OllamaFetch = async () => {
|
|
488
|
+
throw new Error("connection refused");
|
|
489
|
+
};
|
|
490
|
+
await expect(
|
|
491
|
+
ensureOllamaModel({
|
|
492
|
+
url: "http://127.0.0.1:59999",
|
|
493
|
+
model: "qwen3.5:9b",
|
|
494
|
+
fetch: fetchFn,
|
|
495
|
+
readyTimeoutMs: 800,
|
|
496
|
+
}),
|
|
497
|
+
).rejects.toBeInstanceOf(OllamaPullError);
|
|
498
|
+
});
|
|
499
|
+
|
|
247
500
|
test("a new image recipe is ≤15 lines", async () => {
|
|
248
501
|
const src = await Bun.file(`${import.meta.dir}/recipes/postgres.ts`).text();
|
|
249
502
|
const exportLines = src
|
|
@@ -519,18 +772,57 @@ describe("deriveInfrastructure", () => {
|
|
|
519
772
|
expect(result.stackEnv.DATABASE_URL).toBeUndefined();
|
|
520
773
|
});
|
|
521
774
|
|
|
522
|
-
test("ai ollama emits OKE_AI_URL and
|
|
775
|
+
test("ai ollama emits OKE_AI_URL, loopback publish, and never binds 0.0.0.0 on the host", () => {
|
|
523
776
|
const result = deriveInfrastructure({
|
|
524
|
-
images: { ai:
|
|
777
|
+
images: { ai: OLLAMA_IMAGE },
|
|
525
778
|
app: "skyport",
|
|
526
779
|
});
|
|
527
780
|
const yml = result.files.find((f) => f.path === "compose.ai.yml")!.content;
|
|
528
|
-
expect(yml).toContain(
|
|
781
|
+
expect(yml).toContain(OLLAMA_IMAGE);
|
|
529
782
|
expect(yml).toContain("OKE_AI_MODEL");
|
|
530
783
|
expect(yml).toContain("qwen3.5:9b");
|
|
784
|
+
expect(yml).toContain("127.0.0.1:11434:11434");
|
|
785
|
+
expect(yml).not.toMatch(/ports:\s*\n\s*-\s*"?11434:11434"?/);
|
|
531
786
|
expect(result.stackEnv.OKE_AI_URL).toBe("http://127.0.0.1:11434");
|
|
532
787
|
const envText = formatStackEnv(result.stackEnv);
|
|
533
|
-
expect(envText).toContain("# ── ai —
|
|
788
|
+
expect(envText).toContain("# ── ai — local inference");
|
|
789
|
+
expect(envText.match(/^OKE_AI_URL=/gm)?.length).toBe(1);
|
|
790
|
+
});
|
|
791
|
+
|
|
792
|
+
test("ai llama-cpp (default) emits /v1 URL and loopback-only publish", () => {
|
|
793
|
+
const result = deriveInfrastructure({
|
|
794
|
+
images: { ai: LLAMA_CPP_IMAGE },
|
|
795
|
+
app: "skyport",
|
|
796
|
+
controls: { OKE_AI_MODEL: "gemma4:e4b-q4_K_M" },
|
|
797
|
+
});
|
|
798
|
+
const yml = result.files.find((f) => f.path === "compose.ai.yml")!.content;
|
|
799
|
+
expect(yml).toContain(LLAMA_CPP_IMAGE);
|
|
800
|
+
expect(yml).toContain("127.0.0.1:8080:8080");
|
|
801
|
+
expect(yml).toContain("llama-entrypoint.py");
|
|
802
|
+
expect(yml).toContain("/oke/llama-entrypoint.py");
|
|
803
|
+
expect(yml).not.toMatch(/ports:\s*\n\s*-\s*"?8080:8080"?/);
|
|
804
|
+
expect(result.stackEnv.OKE_AI_URL).toBe("http://127.0.0.1:8080/v1");
|
|
805
|
+
expect(result.stackEnv.OKE_AI_MODEL).toBe("gemma4:e4b-q4_K_M");
|
|
806
|
+
const entry = result.files.find((f) => f.path === "llama-entrypoint.py")!.content;
|
|
807
|
+
expect(entry).toContain("llama download");
|
|
808
|
+
expect(entry).toContain("org.cncf.model.filepath");
|
|
809
|
+
expect(entry).toContain("CNCF / Hub registry pull");
|
|
810
|
+
expect(entry).toContain("llama-server");
|
|
811
|
+
expect(entry).toContain("--alias");
|
|
812
|
+
expect(yml).not.toContain("--models-preset");
|
|
813
|
+
});
|
|
814
|
+
|
|
815
|
+
test("ai vllm and sglang emit loopback publish + GPU deploy", () => {
|
|
816
|
+
for (const [image, port, path] of [
|
|
817
|
+
[VLLM_IMAGE, 8000, "compose.ai.yml"],
|
|
818
|
+
[SGLANG_IMAGE, 30000, "compose.ai.yml"],
|
|
819
|
+
] as const) {
|
|
820
|
+
const result = deriveInfrastructure({ images: { ai: image }, app: "skyport" });
|
|
821
|
+
const yml = result.files.find((f) => f.path === path)!.content;
|
|
822
|
+
expect(yml).toContain(`127.0.0.1:${port}:${port}`);
|
|
823
|
+
expect(yml).toContain("nvidia");
|
|
824
|
+
expect(result.stackEnv.OKE_AI_URL).toBe(`http://127.0.0.1:${port}/v1`);
|
|
825
|
+
}
|
|
534
826
|
});
|
|
535
827
|
|
|
536
828
|
test("emits protocol-specific env keys plus optional control notes", () => {
|
package/src/docker/helpers.ts
CHANGED
|
@@ -15,6 +15,62 @@ export function credEnv(spec: ServiceSpec, field: "USER" | "PASSWORD" | "DB"): s
|
|
|
15
15
|
return `\${${key}}`;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
/** Postgres-protocol env shape (`POSTGRES_*`) — shared by postgres / supabase / timescale. */
|
|
19
|
+
export function postgresEnv(s: ServiceSpec): Record<string, string> {
|
|
20
|
+
return {
|
|
21
|
+
POSTGRES_USER: credEnv(s, "USER"),
|
|
22
|
+
POSTGRES_PASSWORD: credEnv(s, "PASSWORD"),
|
|
23
|
+
POSTGRES_DB: credEnv(s, "DB"),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Postgres-protocol healthcheck — shared by postgres / supabase / timescale. */
|
|
28
|
+
export const postgresHealth = {
|
|
29
|
+
test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER"],
|
|
30
|
+
interval: "5s",
|
|
31
|
+
timeout: "3s",
|
|
32
|
+
retries: 10,
|
|
33
|
+
} as const;
|
|
34
|
+
|
|
35
|
+
/** CockroachDB single-node env (`COCKROACH_*`) — used only when the data dir is empty. */
|
|
36
|
+
export function cockroachEnv(s: ServiceSpec): Record<string, string> {
|
|
37
|
+
return {
|
|
38
|
+
COCKROACH_USER: credEnv(s, "USER"),
|
|
39
|
+
COCKROACH_PASSWORD: credEnv(s, "PASSWORD"),
|
|
40
|
+
COCKROACH_DATABASE: credEnv(s, "DB"),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** CockroachDB readiness — DB Console `/health?ready=1`. */
|
|
45
|
+
export const cockroachHealth = {
|
|
46
|
+
test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:8080/health?ready=1 >/dev/null 2>&1 || exit 1"],
|
|
47
|
+
interval: "5s",
|
|
48
|
+
timeout: "5s",
|
|
49
|
+
retries: 20,
|
|
50
|
+
start_period: "15s",
|
|
51
|
+
} as const;
|
|
52
|
+
|
|
53
|
+
/** YugabyteDB YSQL env — `YSQL_PASSWORD` enables authentication automatically. */
|
|
54
|
+
export function yugabyteEnv(s: ServiceSpec): Record<string, string> {
|
|
55
|
+
return {
|
|
56
|
+
YSQL_USER: credEnv(s, "USER"),
|
|
57
|
+
YSQL_PASSWORD: credEnv(s, "PASSWORD"),
|
|
58
|
+
YSQL_DB: credEnv(s, "DB"),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** YugabyteDB YSQL readiness — cluster boot is slow; long `start_period`. */
|
|
63
|
+
export const yugabyteHealth = {
|
|
64
|
+
test: [
|
|
65
|
+
"CMD-SHELL",
|
|
66
|
+
'PGPASSWORD="$$YSQL_PASSWORD" bin/ysqlsh -h 127.0.0.1 -p 5433 -U "$$YSQL_USER" -d "$$YSQL_DB" -c "SELECT 1" >/dev/null 2>&1 || exit 1',
|
|
67
|
+
],
|
|
68
|
+
interval: "10s",
|
|
69
|
+
timeout: "10s",
|
|
70
|
+
retries: 30,
|
|
71
|
+
start_period: "90s",
|
|
72
|
+
} as const;
|
|
73
|
+
|
|
18
74
|
/**
|
|
19
75
|
* Compose service name for a role (`store.sql` → `store-sql`).
|
|
20
76
|
*
|
package/src/docker/index.ts
CHANGED
|
@@ -15,6 +15,7 @@ export type {
|
|
|
15
15
|
ImageRecipe,
|
|
16
16
|
RecipeApplyResult,
|
|
17
17
|
RecipeExtraPort,
|
|
18
|
+
RecipePublishBind,
|
|
18
19
|
RecipeUlimit,
|
|
19
20
|
ServiceCredentials,
|
|
20
21
|
ServiceEndpoint,
|
|
@@ -39,21 +40,72 @@ export { deriveInfrastructure, writeDerivedFiles } from "./derive.ts";
|
|
|
39
40
|
export { emitDockerfile } from "./dockerfile.ts";
|
|
40
41
|
export { credEnv, envPrefix, serviceNameFor } from "./helpers.ts";
|
|
41
42
|
export { generateCredentials } from "./credentials.ts";
|
|
43
|
+
export {
|
|
44
|
+
ensureOllamaModel,
|
|
45
|
+
normalizeOllamaPullUrl,
|
|
46
|
+
ollamaTagsInclude,
|
|
47
|
+
parseOllamaTagsNames,
|
|
48
|
+
OllamaPullError,
|
|
49
|
+
type EnsureOllamaModelOptions,
|
|
50
|
+
type OllamaFetch,
|
|
51
|
+
} from "./ollama-pull.ts";
|
|
52
|
+
export {
|
|
53
|
+
aiModelIdsMatch,
|
|
54
|
+
formatAiModelStatusMessage,
|
|
55
|
+
normalizeAiProbeUrl,
|
|
56
|
+
probeAiModelStatus,
|
|
57
|
+
startAiModelWatch,
|
|
58
|
+
statusFromOllamaTags,
|
|
59
|
+
statusFromOpenAiModels,
|
|
60
|
+
type AiModelFetch,
|
|
61
|
+
type AiModelPhase,
|
|
62
|
+
type AiModelProbeKind,
|
|
63
|
+
type AiModelStatus,
|
|
64
|
+
type ProbeAiModelStatusOptions,
|
|
65
|
+
type StartAiModelWatchOptions,
|
|
66
|
+
} from "./ai-model-status.ts";
|
|
67
|
+
export {
|
|
68
|
+
composeHealthByService,
|
|
69
|
+
composeRowToStatus,
|
|
70
|
+
parseComposePsJson,
|
|
71
|
+
readComposeHealth,
|
|
72
|
+
startComposeHealthWatch,
|
|
73
|
+
watchComposeHealth,
|
|
74
|
+
type ComposePsRow,
|
|
75
|
+
type ReadComposeHealthOptions,
|
|
76
|
+
type StartComposeHealthWatchOptions,
|
|
77
|
+
type WatchComposeHealthOptions,
|
|
78
|
+
} from "./compose-health.ts";
|
|
42
79
|
export {
|
|
43
80
|
builtinRecipes,
|
|
44
81
|
caddy,
|
|
82
|
+
cockroach,
|
|
45
83
|
dragonfly,
|
|
84
|
+
llamaCpp,
|
|
85
|
+
buildLlamaCppEntrypoint,
|
|
86
|
+
LLAMA_CPP_ENTRYPOINT_FILE,
|
|
87
|
+
LLAMA_CPP_ENTRYPOINT_MOUNT,
|
|
88
|
+
LLAMA_CPP_IMAGE,
|
|
89
|
+
LLAMA_CPP_MIN_SAFE_BUILD,
|
|
46
90
|
mailpit,
|
|
47
91
|
meilisearch,
|
|
48
92
|
ollama,
|
|
93
|
+
OLLAMA_IMAGE,
|
|
94
|
+
OLLAMA_MIN_SAFE_VERSION,
|
|
49
95
|
openbao,
|
|
50
96
|
pgdog,
|
|
51
97
|
postgres,
|
|
52
98
|
recipeFor,
|
|
53
99
|
redis,
|
|
54
100
|
rustfs,
|
|
101
|
+
sglang,
|
|
102
|
+
SGLANG_IMAGE,
|
|
103
|
+
timescale,
|
|
55
104
|
traefik,
|
|
56
105
|
valkey,
|
|
106
|
+
vllm,
|
|
107
|
+
VLLM_IMAGE,
|
|
108
|
+
yugabyte,
|
|
57
109
|
} from "./recipes/index.ts";
|
|
58
110
|
export { CADDY_APP_SERVICE, buildCaddyfile } from "./recipes/caddy.ts";
|
|
59
111
|
export { PGDOG_BACKEND_SERVICE, buildPgDogToml, buildPgDogUsersToml } from "./recipes/pgdog.ts";
|