@alienplatform/ai-gateway 3.3.0-dev.0d4f932037814a854d56a7e4e82a8a34cc2e35cb

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.
@@ -0,0 +1,748 @@
1
+ import { createRequire } from "node:module";
2
+ import { AlienError, BindingNotFoundError, InvalidBindingConfigError, InvalidBindingConfigError as InvalidBindingConfigError$1, defineError } from "@alienplatform/core";
3
+ import * as z from "zod/v4";
4
+ import { spawn } from "node:child_process";
5
+ import { chmodSync, existsSync, mkdtempSync, statSync, writeFileSync } from "node:fs";
6
+ import { tmpdir } from "node:os";
7
+ import { dirname, join, resolve } from "node:path";
8
+ import { fileURLToPath } from "node:url";
9
+ //#region src/binding.ts
10
+ /**
11
+ * Parsing for the `ALIEN_<NAME>_BINDING` env var an `ai` resource projects. The client only
12
+ * routes on the binding: a BYO-key (`external`) binding is validated strictly here because the
13
+ * client itself uses its fields, while every other service tag — the ambient variants, including
14
+ * ones added to the platform after this SDK shipped — is passed through for the Rust gateway to
15
+ * validate and serve. Mirrors the Rust `AiBinding` (serde tag "service", lowercase, camelCase
16
+ * fields).
17
+ */
18
+ const externalAiBindingSchema = z.object({
19
+ service: z.literal("external"),
20
+ provider: z.string(),
21
+ apiKey: z.string()
22
+ }).strict();
23
+ /** Narrow an `AiBinding` to the BYO-key variant the client handles itself. */
24
+ function isExternalAiBinding(binding) {
25
+ return binding.service === "external";
26
+ }
27
+ /** The env var an `ai` binding is projected into: `ALIEN_<NAME>_BINDING` (uppercased, `-`→`_`). */
28
+ function aiBindingEnvVarName(name) {
29
+ return `ALIEN_${name.toUpperCase().replace(/-/g, "_")}_BINDING`;
30
+ }
31
+ /** Parse `ALIEN_<NAME>_BINDING`, or `undefined` if it is not set. Throws on malformed content. */
32
+ async function parseAiBinding(name) {
33
+ const raw = process.env[aiBindingEnvVarName(name)];
34
+ if (!raw) return void 0;
35
+ let parsed;
36
+ try {
37
+ parsed = JSON.parse(raw);
38
+ } catch (cause) {
39
+ throw (await AlienError.from(cause)).withContext(InvalidBindingConfigError.create({ message: `AI binding '${name}' is not valid JSON` }));
40
+ }
41
+ const service = parsed !== null && typeof parsed === "object" ? parsed.service : void 0;
42
+ if (typeof service !== "string") throw new AlienError(InvalidBindingConfigError.create({ message: `AI binding '${name}' has no 'service' tag` }));
43
+ if (service !== "external") return parsed;
44
+ const result = externalAiBindingSchema.safeParse(parsed);
45
+ if (!result.success) throw (await AlienError.from(result.error)).withContext(InvalidBindingConfigError.create({ message: `AI binding '${name}' has an unexpected shape` }));
46
+ return result.data;
47
+ }
48
+ //#endregion
49
+ //#region src/errors.ts
50
+ /**
51
+ * Typed errors for the AI client surface (`ai()` / `getAiConnection`).
52
+ *
53
+ * Defined here rather than in `@alienplatform/bindings` because the AI binding
54
+ * resolves through this package's gateway process, not the bindings native addon.
55
+ */
56
+ /**
57
+ * Error thrown when an upstream LLM endpoint returns a non-2xx response.
58
+ *
59
+ * Marked internal because the response body and upstream URL may contain
60
+ * provider-specific infra detail not intended for external clients.
61
+ * Retryability is derived per-call from the HTTP status code.
62
+ */
63
+ const AiUpstreamError = defineError({
64
+ code: "AI_UPSTREAM_ERROR",
65
+ context: z.object({
66
+ url: z.string(),
67
+ status: z.number(),
68
+ message: z.string()
69
+ }),
70
+ message: ({ url, status, message }) => `AI upstream request to '${url}' failed with status ${status}: ${message}`,
71
+ retryable: false,
72
+ internal: true,
73
+ httpStatusCode: 502
74
+ });
75
+ /**
76
+ * Error thrown when a network-level failure occurs before or after an
77
+ * upstream LLM request (fetch threw, or the success body was not parseable JSON).
78
+ * These are always transient and safe to retry.
79
+ */
80
+ const AiTransportError = defineError({
81
+ code: "AI_TRANSPORT_ERROR",
82
+ context: z.object({
83
+ url: z.string(),
84
+ reason: z.string()
85
+ }),
86
+ message: ({ url, reason }) => `AI request to '${url}' failed due to a transport error: ${reason}`,
87
+ retryable: true,
88
+ internal: true,
89
+ httpStatusCode: 503
90
+ });
91
+ /**
92
+ * Error thrown when a BYO-key binding names a provider the client has no base URL for.
93
+ * External (the developer set a bad `provider` tag) and not retryable — failing closed
94
+ * here stops the projected BYO key from being shipped to the wrong provider's endpoint.
95
+ */
96
+ const UnsupportedProviderError = defineError({
97
+ code: "AI_UNSUPPORTED_PROVIDER",
98
+ context: z.object({
99
+ provider: z.string(),
100
+ supported: z.array(z.string())
101
+ }),
102
+ message: ({ provider, supported }) => `Unknown AI provider '${provider}'. Supported providers are ${supported.join(", ")}. For any other OpenAI-compatible provider, set ALIEN_AI_LOCAL_BASE_URL to its base URL.`,
103
+ retryable: false,
104
+ internal: false,
105
+ httpStatusCode: 400
106
+ });
107
+ /**
108
+ * Error thrown when a binding's upstream serves chat/completions but not the OpenAI Responses
109
+ * API. Anthropic's OpenAI-compatible host is the case in point: `/v1/chat/completions` works,
110
+ * `/v1/responses` 404s. External (a caller usage error), not retryable.
111
+ */
112
+ const ResponsesApiUnsupportedError = defineError({
113
+ code: "AI_RESPONSES_API_UNSUPPORTED",
114
+ context: z.object({ provider: z.string() }),
115
+ message: ({ provider }) => `The '${provider}' provider serves chat/completions but not the OpenAI Responses API (/v1/responses). Use chat.completions.create, or set ALIEN_AI_LOCAL_BASE_URL to a Responses-capable endpoint.`,
116
+ retryable: false,
117
+ internal: false,
118
+ httpStatusCode: 400
119
+ });
120
+ /**
121
+ * Error thrown when this platform/architecture has no prebuilt gateway binary.
122
+ */
123
+ const UnsupportedPlatformError = defineError({
124
+ code: "AI_GATEWAY_UNSUPPORTED_PLATFORM",
125
+ context: z.object({
126
+ platform: z.string(),
127
+ arch: z.string(),
128
+ reason: z.string().optional()
129
+ }),
130
+ message: ({ platform, arch, reason }) => `@alienplatform/ai-gateway has no prebuilt binary for platform '${platform}' arch '${arch}'${reason ? `: ${reason}` : ""}.`,
131
+ retryable: false,
132
+ internal: false,
133
+ httpStatusCode: 400
134
+ });
135
+ /**
136
+ * Error thrown when the `alien-ai-gateway` executable cannot be located (or, for a
137
+ * compiled Worker, extracted) for this platform. Internal because the context
138
+ * carries filesystem paths.
139
+ */
140
+ const GatewayBinaryUnavailableError = defineError({
141
+ code: "AI_GATEWAY_BINARY_UNAVAILABLE",
142
+ context: z.object({
143
+ triple: z.string(),
144
+ reason: z.string(),
145
+ path: z.string().optional()
146
+ }),
147
+ message: ({ triple, reason }) => `Cannot locate the alien-ai-gateway binary for '${triple}': ${reason}`,
148
+ retryable: false,
149
+ internal: true,
150
+ httpStatusCode: 400
151
+ });
152
+ /**
153
+ * Error thrown when the spawned `alien-ai-gateway` process failed to report a
154
+ * ready URL: it exited early or errored on startup. Retryable because the common
155
+ * cause (an ambient cloud credential not yet resolvable) is transient.
156
+ */
157
+ const GatewayStartFailedError = defineError({
158
+ code: "AI_GATEWAY_START_FAILED",
159
+ context: z.object({ reason: z.string() }),
160
+ message: ({ reason }) => `The alien-ai-gateway process failed to start: ${reason}`,
161
+ retryable: true,
162
+ internal: true,
163
+ httpStatusCode: 503
164
+ });
165
+ //#endregion
166
+ //#region src/client.ts
167
+ /**
168
+ * AI binding — a thin OpenAI-compatible client to the workload's AI gateway.
169
+ *
170
+ * For an ambient-cloud binding the gateway process is started on the first call; for a
171
+ * BYO-key (External) binding the client talks to the provider directly. Either way the client
172
+ * only forwards OpenAI-shaped requests and streams responses back — it holds no ambient
173
+ * credentials and rewrites nothing (the gateway rewrites the model id and injects the
174
+ * credential).
175
+ */
176
+ const KNOWN_PROVIDER_BASE_URLS = {
177
+ openai: "https://api.openai.com",
178
+ anthropic: "https://api.anthropic.com"
179
+ };
180
+ function providerBaseUrl(provider) {
181
+ const override = process.env.ALIEN_AI_LOCAL_BASE_URL;
182
+ if (override) return override.replace(/\/$/, "");
183
+ const base = KNOWN_PROVIDER_BASE_URLS[provider];
184
+ if (!base) throw new AlienError(UnsupportedProviderError.create({
185
+ provider,
186
+ supported: Object.keys(KNOWN_PROVIDER_BASE_URLS)
187
+ }));
188
+ return base;
189
+ }
190
+ function defaultModels(provider) {
191
+ return provider === "anthropic" ? [
192
+ "claude-opus-4-8",
193
+ "claude-sonnet-5",
194
+ "claude-haiku-4-5"
195
+ ] : ["gpt-4o-mini", "gpt-4o"];
196
+ }
197
+ /**
198
+ * Resolve `ALIEN_<NAME>_BINDING` to a connection target. External -> the provider directly with
199
+ * the projected key. Any other service tag (the ambient variants, including ones this SDK
200
+ * predates) -> the in-process Rust gateway, started here on demand, which validates the binding
201
+ * and injects the ambient cloud credential. Backs both `ai()` and `getAiConnection()`.
202
+ *
203
+ * The returned `baseUrl` is the root without `/v1` — the gateway serves
204
+ * `/<segment>/v1/...`, and the client appends the versioned paths itself.
205
+ */
206
+ async function resolveAiBinding(gateway, name) {
207
+ const binding = await parseAiBinding(name);
208
+ if (!binding) throw new AlienError(BindingNotFoundError.create({
209
+ bindingName: name,
210
+ bindingType: "Ai"
211
+ }));
212
+ if (isExternalAiBinding(binding)) return {
213
+ baseUrl: providerBaseUrl(binding.provider),
214
+ apiKey: binding.apiKey,
215
+ staticModels: defaultModels(binding.provider).map((id) => ({
216
+ id,
217
+ provider: binding.provider,
218
+ displayName: id
219
+ }))
220
+ };
221
+ const handle = await gateway.startAiGateway();
222
+ const segment = name.toLowerCase().replace(/_/g, "-");
223
+ return { baseUrl: `${handle.url}/${segment}` };
224
+ }
225
+ const RETRYABLE_STATUSES = /* @__PURE__ */ new Set([
226
+ 429,
227
+ 502,
228
+ 503,
229
+ 504
230
+ ]);
231
+ function createUpstreamError(url, status, message) {
232
+ return new AlienError({
233
+ ...AiUpstreamError.create({
234
+ url,
235
+ status,
236
+ message
237
+ }).toOptions(),
238
+ retryable: RETRYABLE_STATUSES.has(status),
239
+ httpStatusCode: status >= 400 ? status : 502
240
+ });
241
+ }
242
+ async function* parseSse(url, body) {
243
+ const reader = body.getReader();
244
+ const decoder = new TextDecoder();
245
+ let buffer = "";
246
+ const emit = function* (line) {
247
+ const trimmed = line.trimEnd();
248
+ if (!trimmed.startsWith("data:")) return;
249
+ const payload = trimmed.slice(5).trim();
250
+ if (payload === "[DONE]") return;
251
+ let chunk;
252
+ try {
253
+ chunk = JSON.parse(payload);
254
+ } catch (cause) {
255
+ throw new AlienError({
256
+ ...AiTransportError.create({
257
+ url,
258
+ reason: `Malformed SSE chunk: ${cause instanceof Error ? cause.message : String(cause)}`
259
+ }).toOptions(),
260
+ source: {
261
+ code: "GENERIC_ERROR",
262
+ message: cause instanceof Error ? cause.stack ?? cause.message : String(cause),
263
+ retryable: false,
264
+ internal: true
265
+ }
266
+ });
267
+ }
268
+ yield chunk;
269
+ };
270
+ try {
271
+ while (true) {
272
+ const { done, value } = await reader.read();
273
+ if (done) break;
274
+ buffer += decoder.decode(value, { stream: true });
275
+ const lines = buffer.split("\n");
276
+ buffer = lines.pop() ?? "";
277
+ for (const line of lines) {
278
+ if (line.trimEnd() === "data: [DONE]") return;
279
+ yield* emit(line);
280
+ }
281
+ }
282
+ buffer += decoder.decode();
283
+ for (const line of buffer.split("\n")) {
284
+ if (line.trimEnd() === "data: [DONE]") return;
285
+ yield* emit(line);
286
+ }
287
+ } finally {
288
+ await reader.cancel().catch(() => {});
289
+ }
290
+ }
291
+ /**
292
+ * Constructed by the `ai(name)` factory; resolves the binding (and starts the
293
+ * gateway, for ambient) once on first use.
294
+ *
295
+ * @example
296
+ * ```typescript
297
+ * import { ai } from "@alienplatform/sdk"
298
+ *
299
+ * const llm = ai("my-llm")
300
+ * const response = await llm.chat.completions.create({
301
+ * model: "claude-opus-4.8",
302
+ * messages: [{ role: "user", content: "Hello!" }],
303
+ * })
304
+ * ```
305
+ */
306
+ var Ai = class {
307
+ resolve;
308
+ connectionPromise = null;
309
+ /** OpenAI-compatible chat namespace. */
310
+ chat;
311
+ /** OpenAI-compatible Responses API namespace (the surface Codex speaks). */
312
+ responses;
313
+ constructor(resolve) {
314
+ this.resolve = resolve;
315
+ this.chat = { completions: { create: ((params) => this._chatCompletionsCreate(params)) } };
316
+ this.responses = { create: ((params) => this._responsesCreate(params)) };
317
+ }
318
+ connection() {
319
+ this.connectionPromise ??= this.resolve().catch((error) => {
320
+ this.connectionPromise = null;
321
+ throw error;
322
+ });
323
+ return this.connectionPromise;
324
+ }
325
+ /** List the models the gateway exposes for this binding's cloud. */
326
+ async getAvailableModels() {
327
+ const { baseUrl, staticModels } = await this.connection();
328
+ if (staticModels) return staticModels;
329
+ const url = `${baseUrl}/v1/models`;
330
+ const response = await this._fetch(url, { method: "GET" });
331
+ if (!response.ok) throw createUpstreamError(url, response.status, await extractErrorMessage(response));
332
+ let body;
333
+ try {
334
+ body = await response.json();
335
+ } catch (jsonError) {
336
+ throw (await AlienError.from(jsonError)).withContext(AiTransportError.create({
337
+ url,
338
+ reason: `Response body is not valid JSON: ${jsonError instanceof Error ? jsonError.message : String(jsonError)}`
339
+ }));
340
+ }
341
+ if (!Array.isArray(body.data)) throw createUpstreamError(url, response.status, "models response had no data array");
342
+ for (const model of body.data) if (typeof model?.id !== "string" || typeof model?.provider !== "string" || typeof model?.displayName !== "string") throw createUpstreamError(url, response.status, `models response entry is missing id/provider/displayName: ${JSON.stringify(model)}`);
343
+ return body.data;
344
+ }
345
+ _chatCompletionsCreate(params) {
346
+ return this._postSurface("/v1/chat/completions", params);
347
+ }
348
+ async _responsesCreate(params) {
349
+ const { baseUrl } = await this.connection();
350
+ if (baseUrl === KNOWN_PROVIDER_BASE_URLS.anthropic) throw new AlienError(ResponsesApiUnsupportedError.create({ provider: "anthropic" }));
351
+ return this._postSurface("/v1/responses", params);
352
+ }
353
+ async _postSurface(path, params) {
354
+ const { baseUrl, apiKey } = await this.connection();
355
+ const url = `${baseUrl}${path}`;
356
+ const response = await this._fetch(url, {
357
+ method: "POST",
358
+ headers: {
359
+ "Content-Type": "application/json",
360
+ ...apiKey ? { Authorization: `Bearer ${apiKey}` } : {}
361
+ },
362
+ body: JSON.stringify(params)
363
+ });
364
+ if (!response.ok) throw createUpstreamError(url, response.status, await extractErrorMessage(response));
365
+ if (params.stream === true) {
366
+ if (!response.body) throw createUpstreamError(url, response.status, "Streaming response body is null");
367
+ return parseSse(url, response.body);
368
+ }
369
+ try {
370
+ return await response.json();
371
+ } catch (jsonError) {
372
+ throw (await AlienError.from(jsonError)).withContext(AiTransportError.create({
373
+ url,
374
+ reason: `Response body is not valid JSON: ${jsonError instanceof Error ? jsonError.message : String(jsonError)}`
375
+ }));
376
+ }
377
+ }
378
+ async _fetch(url, init) {
379
+ try {
380
+ return await fetch(url, init);
381
+ } catch (fetchError) {
382
+ throw (await AlienError.from(fetchError)).withContext(AiTransportError.create({
383
+ url,
384
+ reason: fetchError instanceof Error ? fetchError.message : String(fetchError)
385
+ }));
386
+ }
387
+ }
388
+ };
389
+ /**
390
+ * Build the `ai()` / `getAiConnection()` pair over a gateway. Mirrors `createGateway`:
391
+ * each entry wires its own addon acquisition and shares this implementation.
392
+ */
393
+ function createAiClient(gateway) {
394
+ return {
395
+ /** An OpenAI-compatible client for the named AI binding. */
396
+ ai(name) {
397
+ return new Ai(() => resolveAiBinding(gateway, name));
398
+ },
399
+ /**
400
+ * Resolve an AI binding to `{ baseURL, apiKey? }` for a plain OpenAI-compatible client
401
+ * (e.g. the Vercel AI SDK's `createOpenAICompatible`). The binding decides the target,
402
+ * so app code is identical whether AI is a BYO-key provider (local) or an ambient-cloud
403
+ * model behind the gateway.
404
+ *
405
+ * Awaits gateway startup for an ambient binding, so the returned `baseURL` is live
406
+ * before the caller's client uses it.
407
+ */
408
+ async getAiConnection(name) {
409
+ const resolved = await resolveAiBinding(gateway, name);
410
+ return {
411
+ baseURL: `${resolved.baseUrl}/v1`,
412
+ apiKey: resolved.apiKey
413
+ };
414
+ }
415
+ };
416
+ }
417
+ async function extractErrorMessage(response) {
418
+ try {
419
+ const errBody = await response.json();
420
+ const errObj = errBody.error;
421
+ if (errObj && typeof errObj === "object" && "message" in errObj) return String(errObj.message);
422
+ return errBody.message ? String(errBody.message) : response.statusText;
423
+ } catch {
424
+ return response.statusText || "Unknown upstream error";
425
+ }
426
+ }
427
+ //#endregion
428
+ //#region src/loader.ts
429
+ /**
430
+ * Locate (and, for a compiled Worker, extract) the `alien-ai-gateway` executable
431
+ * that the gateway wrapper spawns.
432
+ *
433
+ * Resolution order (deferred to first spawn, so importing the package does no I/O):
434
+ *
435
+ * 1. An embedded binary registered by the `./native` entry. A `bun build
436
+ * --compile` Worker embeds the executable and hands us its (virtual) path;
437
+ * we copy it to a real, executable temp file once.
438
+ * 2. `ALIEN_AI_GATEWAY_BINARY_PATH`: an explicit path (dev/test escape hatch;
439
+ * never set in published installs).
440
+ * 3. `./alien-ai-gateway` in the working directory: the versioned Worker base
441
+ * stages it there for bundled Workers, which have no node_modules.
442
+ * 4. The per-platform prebuild package `@alienplatform/ai-gateway-<triple>`,
443
+ * which ships the executable (installed via `optionalDependencies`).
444
+ * 5. Dev fallback: the locally-built binary under `target/{release,debug}`,
445
+ * found by walking up from this module.
446
+ */
447
+ const require = createRequire(import.meta.url);
448
+ /** The launcher binary's file name across all platforms. */
449
+ const BINARY_NAME = "alien-ai-gateway";
450
+ /** Detect glibc vs musl, the same way napi-rs's generated loader does. */
451
+ function detectLinuxLibc() {
452
+ const report = typeof process.report?.getReport === "function" ? process.report.getReport() : void 0;
453
+ if (report?.header) return report.header.glibcVersionRuntime ? "gnu" : "musl";
454
+ return existsSync("/etc/alpine-release") ? "musl" : "gnu";
455
+ }
456
+ /**
457
+ * Map `process.platform`/`arch` to the prebuild triple. The binary is built for
458
+ * musl as well as glibc, so Alpine/musl images resolve a prebuild rather than
459
+ * being rejected for their libc.
460
+ */
461
+ function platformTriple(platform = process.platform, arch = process.arch, libc = platform === "linux" ? detectLinuxLibc() : "gnu") {
462
+ if (platform === "darwin" && arch === "arm64") return "darwin-arm64";
463
+ if (platform === "darwin" && arch === "x64") return "darwin-x64";
464
+ if (platform === "linux") {
465
+ if (arch === "x64") return `linux-x64-${libc}`;
466
+ if (arch === "arm64") return `linux-arm64-${libc}`;
467
+ }
468
+ throw new AlienError(UnsupportedPlatformError.create({
469
+ platform,
470
+ arch
471
+ }));
472
+ }
473
+ /** Walk up from `startDir` to find the locally-built binary, or `undefined`. */
474
+ function findLocalBinary(startDir = dirname(fileURLToPath(import.meta.url))) {
475
+ let dir = startDir;
476
+ for (;;) {
477
+ for (const profile of ["release", "debug"]) {
478
+ const candidate = join(dir, "target", profile, BINARY_NAME);
479
+ if (existsSync(candidate)) return candidate;
480
+ }
481
+ const parent = dirname(dir);
482
+ if (parent === dir) return void 0;
483
+ dir = parent;
484
+ }
485
+ }
486
+ let cached;
487
+ let embeddedBinaryPath;
488
+ /**
489
+ * Register the bun-embedded binary path, so plain `@alienplatform/ai-gateway`
490
+ * imports (which resolve through {@link resolveGatewayBinary}) find it inside a
491
+ * `bun build --compile` binary, where the filesystem/prebuild resolution below
492
+ * cannot. In a normal install this is never called. The `/native` entry calls it.
493
+ */
494
+ function registerEmbeddedBinary(path) {
495
+ embeddedBinaryPath = path;
496
+ }
497
+ /** Resolve (and memoize) a runnable path to the launcher binary, or throw. */
498
+ async function resolveGatewayBinary() {
499
+ if (cached) return cached;
500
+ if (embeddedBinaryPath) {
501
+ cached = await extractEmbedded(embeddedBinaryPath);
502
+ return cached;
503
+ }
504
+ const override = process.env.ALIEN_AI_GATEWAY_BINARY_PATH;
505
+ if (override) {
506
+ cached = ensureExecutable(resolve(override));
507
+ return cached;
508
+ }
509
+ const staged = resolve(BINARY_NAME);
510
+ if (existsSync(staged) && statSync(staged).size > 0) {
511
+ cached = ensureExecutable(staged);
512
+ return cached;
513
+ }
514
+ const triple = platformTriple();
515
+ const prebuild = await resolvePrebuild(triple);
516
+ if (prebuild) {
517
+ cached = ensureExecutable(prebuild);
518
+ return cached;
519
+ }
520
+ const local = findLocalBinary();
521
+ if (local) {
522
+ cached = ensureExecutable(local);
523
+ return cached;
524
+ }
525
+ throw new AlienError(GatewayBinaryUnavailableError.create({
526
+ triple,
527
+ reason: `no embedded binary, no ALIEN_AI_GATEWAY_BINARY_PATH, no ./${BINARY_NAME} staged by the worker base, no '@alienplatform/ai-gateway-${triple}' prebuild, and no locally-built target/{release,debug}/${BINARY_NAME}; build it with \`cargo build --bin ${BINARY_NAME} -p alien-ai-gateway\``
528
+ }));
529
+ }
530
+ /**
531
+ * Copy a bun-embedded binary to a real, executable temp file. Only reached inside
532
+ * a `bun build --compile` binary, where `Bun` is present and the embedded file
533
+ * must live on disk with the execute bit set before it can be spawned.
534
+ */
535
+ async function extractEmbedded(virtualPath) {
536
+ const bun = globalThis.Bun;
537
+ if (!bun) throw new AlienError(GatewayBinaryUnavailableError.create({
538
+ triple: platformTriple(),
539
+ path: virtualPath,
540
+ reason: "an embedded gateway binary can only be extracted under the Bun runtime"
541
+ }));
542
+ try {
543
+ const bytes = await bun.file(virtualPath).arrayBuffer();
544
+ const exe = join(mkdtempSync(join(tmpdir(), "alien-ai-gateway-")), BINARY_NAME);
545
+ writeFileSync(exe, Buffer.from(bytes));
546
+ chmodSync(exe, 493);
547
+ return exe;
548
+ } catch (error) {
549
+ throw (await AlienError.from(error)).withContext(GatewayBinaryUnavailableError.create({
550
+ triple: platformTriple(),
551
+ path: virtualPath,
552
+ reason: "failed to extract the embedded gateway binary"
553
+ }));
554
+ }
555
+ }
556
+ /** Best-effort execute bit; a prebuild may already be executable or read-only. */
557
+ function ensureExecutable(path) {
558
+ try {
559
+ chmodSync(path, 493);
560
+ } catch {}
561
+ return path;
562
+ }
563
+ /** Resolve the binary shipped by the per-platform prebuild package, or `undefined`. */
564
+ async function resolvePrebuild(triple) {
565
+ const pkg = `@alienplatform/ai-gateway-${triple}`;
566
+ let pkgJson;
567
+ try {
568
+ pkgJson = require.resolve(`${pkg}/package.json`);
569
+ } catch (error) {
570
+ if (error.code === "MODULE_NOT_FOUND") return void 0;
571
+ throw (await AlienError.from(error)).withContext(GatewayBinaryUnavailableError.create({
572
+ triple,
573
+ reason: `the '${pkg}' prebuild is installed but could not be resolved`
574
+ }));
575
+ }
576
+ const candidate = join(dirname(pkgJson), BINARY_NAME);
577
+ return existsSync(candidate) ? candidate : void 0;
578
+ }
579
+ //#endregion
580
+ //#region src/gateway.ts
581
+ /**
582
+ * The gateway surface: obtain the loopback URL of the Alien AI gateway.
583
+ *
584
+ * Two paths, unified behind {@link Gateway.startAiGateway}:
585
+ * - A container launcher already started the gateway out of band and exec'd the
586
+ * app with `ALIEN_AI_GATEWAY_URL` set, so we use that URL directly.
587
+ * - Otherwise (an SDK Worker) we spawn the `alien-ai-gateway` binary ourselves,
588
+ * read the URL it prints, and keep the child alive for the process lifetime.
589
+ *
590
+ * Parameterized over how the binary is resolved so the lazy entry (`index.ts`) and
591
+ * the compiled-embed entry (`native.ts`) share one implementation.
592
+ */
593
+ /** The env var a container launcher sets after starting the gateway out of band. */
594
+ const URL_ENV = "ALIEN_AI_GATEWAY_URL";
595
+ /** The launcher serve mode; binds an ephemeral port and prints its URL. */
596
+ const SERVE_FLAG = "--gateway-serve";
597
+ function createGateway(resolveBinary) {
598
+ let started = null;
599
+ const forget = () => {
600
+ started = null;
601
+ };
602
+ async function startAiGateway() {
603
+ started ??= startOnce(resolveBinary, forget).catch((error) => {
604
+ forget();
605
+ throw error;
606
+ });
607
+ return started;
608
+ }
609
+ return { startAiGateway };
610
+ }
611
+ async function startOnce(resolveBinary, onGatewayLost) {
612
+ const preset = process.env[URL_ENV];
613
+ if (preset) return { url: preset };
614
+ const binary = await resolveBinary();
615
+ const child = spawn(binary, [SERVE_FLAG], { stdio: [
616
+ "ignore",
617
+ "pipe",
618
+ "pipe"
619
+ ] });
620
+ trackChild(child);
621
+ const url = await readReadyUrl(child, binary);
622
+ keepChildAlive(child, onGatewayLost);
623
+ return { url };
624
+ }
625
+ /**
626
+ * Read the gateway's `{"aiGatewayUrl":"..."}` line from stdout. Rejects (with the
627
+ * child's stderr as the reason) if the process exits or errors before printing it.
628
+ */
629
+ function readReadyUrl(child, binary) {
630
+ return new Promise((resolveUrl, rejectUrl) => {
631
+ let stdout = "";
632
+ let stderr = "";
633
+ const cleanup = () => {
634
+ child.stdout?.off("data", onStdout);
635
+ child.stderr?.off("data", onStderr);
636
+ child.off("exit", onExit);
637
+ child.off("error", onError);
638
+ };
639
+ const onStdout = (chunk) => {
640
+ stdout += chunk.toString();
641
+ const url = parseReadyUrl(stdout);
642
+ if (url) {
643
+ cleanup();
644
+ resolveUrl(url);
645
+ }
646
+ };
647
+ const onStderr = (chunk) => {
648
+ stderr += chunk.toString();
649
+ };
650
+ const onExit = (code, signal) => {
651
+ cleanup();
652
+ rejectUrl(new AlienError(GatewayStartFailedError.create({ reason: stderr.trim() || `process exited (${signal ?? `code ${code ?? "null"}`}) before reporting a URL` })));
653
+ };
654
+ const onError = async (error) => {
655
+ cleanup();
656
+ let triple;
657
+ try {
658
+ triple = platformTriple();
659
+ } catch {
660
+ triple = `${process.platform}-${process.arch}`;
661
+ }
662
+ rejectUrl((await AlienError.from(error)).withContext(GatewayBinaryUnavailableError.create({
663
+ triple,
664
+ path: binary,
665
+ reason: "could not execute the gateway binary"
666
+ })));
667
+ };
668
+ child.stdout?.on("data", onStdout);
669
+ child.stderr?.on("data", onStderr);
670
+ child.on("exit", onExit);
671
+ child.on("error", onError);
672
+ });
673
+ }
674
+ /** Extract the URL from the launcher's machine-readable stdout line, if present. */
675
+ function parseReadyUrl(buffered) {
676
+ for (const line of buffered.split("\n")) {
677
+ const trimmed = line.trim();
678
+ if (!trimmed.startsWith("{")) continue;
679
+ try {
680
+ const parsed = JSON.parse(trimmed);
681
+ if (typeof parsed.aiGatewayUrl === "string") return parsed.aiGatewayUrl;
682
+ } catch {}
683
+ }
684
+ }
685
+ let liveChild;
686
+ let reaperInstalled = false;
687
+ /** Reap whatever child is live when this process is torn down. Installed once. */
688
+ function installReaper() {
689
+ if (reaperInstalled) return;
690
+ reaperInstalled = true;
691
+ const killLive = () => {
692
+ try {
693
+ liveChild?.kill("SIGTERM");
694
+ } catch {}
695
+ };
696
+ process.once("exit", killLive);
697
+ for (const signal of ["SIGINT", "SIGTERM"]) process.once(signal, () => {
698
+ killLive();
699
+ process.kill(process.pid, signal);
700
+ });
701
+ }
702
+ /**
703
+ * Track a freshly spawned child as the live gateway and guard it synchronously, at
704
+ * spawn time (before the ready-await). A live child stream with no 'error' listener
705
+ * throws on any I/O fault and crashes the host, so attach permanent no-op stream
706
+ * guards now (`readReadyUrl` only adds 'data' listeners; the child itself always has
707
+ * an 'error' listener, `readReadyUrl`'s during startup then `keepChildAlive`'s after).
708
+ */
709
+ function trackChild(child) {
710
+ liveChild = child;
711
+ installReaper();
712
+ child.stdout?.on("error", () => {});
713
+ child.stderr?.on("error", () => {});
714
+ }
715
+ /**
716
+ * Unref a child stdio stream's underlying handle. `stdout`/`stderr` are typed `Readable` but are
717
+ * `net.Socket` at runtime and carry `.unref()`; the guard makes a non-socket or absent stream a
718
+ * no-op instead of a crash.
719
+ */
720
+ function unrefStream(stream) {
721
+ const s = stream;
722
+ if (s && typeof s.unref === "function") s.unref();
723
+ }
724
+ /**
725
+ * Once the child is serving, hold it for the app's lifetime: drain its stdio so the
726
+ * pipes never fill and block it, then unref both the stdio sockets and the child so it
727
+ * never keeps the event loop alive on its own. `resume()` keeps the pipe sockets
728
+ * referenced, so unref-ing the child alone leaves a one-shot host (a CLI/batch/test
729
+ * process that calls `ai()` once) unable to exit — the sockets must be unref'd too. On a
730
+ * later exit/error, forget the memoized handle so the next call respawns rather than
731
+ * reusing a dead URL (`trackChild` already installed the reaper and stream guards at
732
+ * spawn time).
733
+ */
734
+ function keepChildAlive(child, onGatewayLost) {
735
+ const onGone = () => {
736
+ if (liveChild === child) liveChild = void 0;
737
+ onGatewayLost();
738
+ };
739
+ child.on("error", onGone);
740
+ child.on("exit", onGone);
741
+ child.stdout?.resume();
742
+ child.stderr?.resume();
743
+ unrefStream(child.stdout);
744
+ unrefStream(child.stderr);
745
+ child.unref();
746
+ }
747
+ //#endregion
748
+ export { createAiClient as a, BindingNotFoundError as c, isExternalAiBinding as d, parseAiBinding as f, Ai as i, InvalidBindingConfigError$1 as l, registerEmbeddedBinary as n, AiTransportError as o, resolveGatewayBinary as r, AiUpstreamError as s, createGateway as t, aiBindingEnvVarName as u };