@valni/cli-darwin-x64 0.0.0-stage → 0.1.3

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.
Files changed (183) hide show
  1. package/LICENSE +52 -0
  2. package/package.json +18 -5
  3. package/runtime/LICENSE +52 -0
  4. package/runtime/assets/clankolas.png +0 -0
  5. package/runtime/examples/extensions/auto-commit-on-exit.ts +49 -0
  6. package/runtime/examples/extensions/bash-spawn-hook.ts +30 -0
  7. package/runtime/examples/extensions/bookmark.ts +50 -0
  8. package/runtime/examples/extensions/border-status-editor.ts +145 -0
  9. package/runtime/examples/extensions/built-in-tool-renderer.ts +249 -0
  10. package/runtime/examples/extensions/claude-rules.ts +86 -0
  11. package/runtime/examples/extensions/commands.ts +72 -0
  12. package/runtime/examples/extensions/confirm-destructive.ts +59 -0
  13. package/runtime/examples/extensions/custom-footer.ts +64 -0
  14. package/runtime/examples/extensions/custom-header.ts +73 -0
  15. package/runtime/examples/extensions/custom-provider-anthropic/index.ts +604 -0
  16. package/runtime/examples/extensions/custom-provider-anthropic/package-lock.json +24 -0
  17. package/runtime/examples/extensions/custom-provider-anthropic/package.json +19 -0
  18. package/runtime/examples/extensions/dirty-repo-guard.ts +56 -0
  19. package/runtime/examples/extensions/doom-overlay/doom/build/doom.js +21 -0
  20. package/runtime/examples/extensions/doom-overlay/doom/build/doom.wasm +0 -0
  21. package/runtime/examples/extensions/doom-overlay/doom/build.sh +152 -0
  22. package/runtime/examples/extensions/doom-overlay/doom/doomgeneric_valni.c +72 -0
  23. package/runtime/examples/extensions/doom-overlay/doom-component.ts +132 -0
  24. package/runtime/examples/extensions/doom-overlay/doom-engine.ts +173 -0
  25. package/runtime/examples/extensions/doom-overlay/doom-keys.ts +104 -0
  26. package/runtime/examples/extensions/doom-overlay/index.ts +74 -0
  27. package/runtime/examples/extensions/doom-overlay/wad-finder.ts +51 -0
  28. package/runtime/examples/extensions/dynamic-resources/dynamic.json +78 -0
  29. package/runtime/examples/extensions/dynamic-resources/index.ts +13 -0
  30. package/runtime/examples/extensions/dynamic-tools.ts +74 -0
  31. package/runtime/examples/extensions/entry-renderer.ts +41 -0
  32. package/runtime/examples/extensions/event-bus.ts +43 -0
  33. package/runtime/examples/extensions/file-trigger.ts +41 -0
  34. package/runtime/examples/extensions/git-checkpoint.ts +53 -0
  35. package/runtime/examples/extensions/git-merge-and-resolve.ts +115 -0
  36. package/runtime/examples/extensions/github-issue-autocomplete.ts +185 -0
  37. package/runtime/examples/extensions/hello.ts +26 -0
  38. package/runtime/examples/extensions/hidden-thinking-label.ts +53 -0
  39. package/runtime/examples/extensions/inline-bash.ts +94 -0
  40. package/runtime/examples/extensions/input-transform-streaming.ts +39 -0
  41. package/runtime/examples/extensions/input-transform.ts +43 -0
  42. package/runtime/examples/extensions/interactive-shell.ts +196 -0
  43. package/runtime/examples/extensions/mac-system-theme.ts +47 -0
  44. package/runtime/examples/extensions/message-renderer.ts +59 -0
  45. package/runtime/examples/extensions/minimal-mode.ts +426 -0
  46. package/runtime/examples/extensions/modal-editor.ts +85 -0
  47. package/runtime/examples/extensions/model-status.ts +31 -0
  48. package/runtime/examples/extensions/notify.ts +55 -0
  49. package/runtime/examples/extensions/overlay-qa-tests.ts +1450 -0
  50. package/runtime/examples/extensions/overlay-test.ts +153 -0
  51. package/runtime/examples/extensions/permission-gate.ts +34 -0
  52. package/runtime/examples/extensions/pirate.ts +47 -0
  53. package/runtime/examples/extensions/plan-mode/index.ts +390 -0
  54. package/runtime/examples/extensions/plan-mode/utils.ts +168 -0
  55. package/runtime/examples/extensions/preset.ts +436 -0
  56. package/runtime/examples/extensions/project-trust.ts +64 -0
  57. package/runtime/examples/extensions/prompt-customizer.ts +97 -0
  58. package/runtime/examples/extensions/protected-paths.ts +30 -0
  59. package/runtime/examples/extensions/provider-payload.ts +18 -0
  60. package/runtime/examples/extensions/question.ts +278 -0
  61. package/runtime/examples/extensions/questionnaire.ts +440 -0
  62. package/runtime/examples/extensions/rainbow-editor.ts +88 -0
  63. package/runtime/examples/extensions/reload-runtime.ts +37 -0
  64. package/runtime/examples/extensions/rpc-demo.ts +118 -0
  65. package/runtime/examples/extensions/sandbox/index.ts +321 -0
  66. package/runtime/examples/extensions/sandbox/package-lock.json +92 -0
  67. package/runtime/examples/extensions/sandbox/package.json +19 -0
  68. package/runtime/examples/extensions/send-user-message.ts +97 -0
  69. package/runtime/examples/extensions/session-name.ts +27 -0
  70. package/runtime/examples/extensions/shutdown-command.ts +63 -0
  71. package/runtime/examples/extensions/snake.ts +343 -0
  72. package/runtime/examples/extensions/space-invaders.ts +560 -0
  73. package/runtime/examples/extensions/ssh.ts +220 -0
  74. package/runtime/examples/extensions/status-line.ts +32 -0
  75. package/runtime/examples/extensions/structured-output.ts +65 -0
  76. package/runtime/examples/extensions/subagent/agents.ts +126 -0
  77. package/runtime/examples/extensions/subagent/index.ts +1009 -0
  78. package/runtime/examples/extensions/system-prompt-header.ts +17 -0
  79. package/runtime/examples/extensions/tic-tac-toe.ts +1008 -0
  80. package/runtime/examples/extensions/timed-confirm.ts +70 -0
  81. package/runtime/examples/extensions/titlebar-spinner.ts +58 -0
  82. package/runtime/examples/extensions/todo.ts +297 -0
  83. package/runtime/examples/extensions/tool-override.ts +144 -0
  84. package/runtime/examples/extensions/tools.ts +146 -0
  85. package/runtime/examples/extensions/trigger-compact.ts +50 -0
  86. package/runtime/examples/extensions/truncated-tool.ts +195 -0
  87. package/runtime/examples/extensions/widget-placement.ts +9 -0
  88. package/runtime/examples/extensions/with-deps/index.ts +32 -0
  89. package/runtime/examples/extensions/with-deps/package-lock.json +31 -0
  90. package/runtime/examples/extensions/with-deps/package.json +22 -0
  91. package/runtime/examples/extensions/working-indicator.ts +123 -0
  92. package/runtime/examples/extensions/working-message-test.ts +25 -0
  93. package/runtime/examples/rpc-extension-ui.ts +632 -0
  94. package/runtime/examples/sdk/01-minimal.ts +26 -0
  95. package/runtime/examples/sdk/02-custom-model.ts +43 -0
  96. package/runtime/examples/sdk/03-custom-prompt.ts +70 -0
  97. package/runtime/examples/sdk/04-skills.ts +55 -0
  98. package/runtime/examples/sdk/05-tools.ts +48 -0
  99. package/runtime/examples/sdk/06-extensions.ts +94 -0
  100. package/runtime/examples/sdk/07-context-files.ts +42 -0
  101. package/runtime/examples/sdk/08-prompt-templates.ts +51 -0
  102. package/runtime/examples/sdk/09-api-keys-and-oauth.ts +34 -0
  103. package/runtime/examples/sdk/10-settings.ts +53 -0
  104. package/runtime/examples/sdk/11-sessions.ts +52 -0
  105. package/runtime/examples/sdk/12-full-control.ts +71 -0
  106. package/runtime/examples/sdk/13-session-runtime.ts +67 -0
  107. package/runtime/export-html/ansi-to-html.js +249 -0
  108. package/runtime/export-html/index.js +226 -0
  109. package/runtime/export-html/template.css +1066 -0
  110. package/runtime/export-html/template.html +55 -0
  111. package/runtime/export-html/template.js +1864 -0
  112. package/runtime/export-html/tool-renderer.js +108 -0
  113. package/runtime/export-html/vendor/highlight.min.js +1213 -0
  114. package/runtime/export-html/vendor/marked.min.js +78 -0
  115. package/runtime/extensions/pi-switchboard/.github/workflows/ci.yml +51 -0
  116. package/runtime/extensions/pi-switchboard/LICENSE +22 -0
  117. package/runtime/extensions/pi-switchboard/README.md +85 -0
  118. package/runtime/extensions/pi-switchboard/extensions/switchboard/catalog.ts +183 -0
  119. package/runtime/extensions/pi-switchboard/extensions/switchboard/config.ts +97 -0
  120. package/runtime/extensions/pi-switchboard/extensions/switchboard/connections.ts +386 -0
  121. package/runtime/extensions/pi-switchboard/extensions/switchboard/constants.ts +12 -0
  122. package/runtime/extensions/pi-switchboard/extensions/switchboard/device-auth.ts +127 -0
  123. package/runtime/extensions/pi-switchboard/extensions/switchboard/envelope.ts +199 -0
  124. package/runtime/extensions/pi-switchboard/extensions/switchboard/errors.ts +180 -0
  125. package/runtime/extensions/pi-switchboard/extensions/switchboard/index.ts +125 -0
  126. package/runtime/extensions/pi-switchboard/extensions/switchboard/participant.ts +17 -0
  127. package/runtime/extensions/pi-switchboard/extensions/switchboard/sessionStream.ts +131 -0
  128. package/runtime/extensions/pi-switchboard/extensions/switchboard/toolProxy.ts +136 -0
  129. package/runtime/extensions/pi-switchboard/extensions/switchboard/tools.ts +16 -0
  130. package/runtime/extensions/pi-switchboard/extensions/switchboard/types.ts +37 -0
  131. package/runtime/extensions/pi-switchboard/package.json +30 -0
  132. package/runtime/extensions/pi-switchboard/tsconfig.json +13 -0
  133. package/runtime/extensions/switchboard-local/.github/workflows/ci.yml +80 -0
  134. package/runtime/extensions/switchboard-local/.github/workflows/release.yml +46 -0
  135. package/runtime/extensions/switchboard-local/README.md +98 -0
  136. package/runtime/extensions/switchboard-local/extensions/local/engine.ts +28 -0
  137. package/runtime/extensions/switchboard-local/extensions/local/engines/gguf.ts +324 -0
  138. package/runtime/extensions/switchboard-local/extensions/local/engines/llamaBuild.ts +62 -0
  139. package/runtime/extensions/switchboard-local/extensions/local/engines/mlx.ts +245 -0
  140. package/runtime/extensions/switchboard-local/extensions/local/engines/select.ts +38 -0
  141. package/runtime/extensions/switchboard-local/extensions/local/engines/support.ts +151 -0
  142. package/runtime/extensions/switchboard-local/extensions/local/hf.ts +106 -0
  143. package/runtime/extensions/switchboard-local/extensions/local/index.ts +67 -0
  144. package/runtime/extensions/switchboard-local/extensions/local/nativeStream.ts +602 -0
  145. package/runtime/extensions/switchboard-local/extensions/local/paths.ts +44 -0
  146. package/runtime/extensions/switchboard-local/extensions/local/provider.ts +57 -0
  147. package/runtime/extensions/switchboard-local/extensions/local/store.ts +82 -0
  148. package/runtime/extensions/switchboard-local/extensions/local/ui.ts +60 -0
  149. package/runtime/extensions/switchboard-local/node_modules/@valni/switchboard/dist/apiTypes.js +74 -0
  150. package/runtime/extensions/switchboard-local/node_modules/@valni/switchboard/dist/client.js +292 -0
  151. package/runtime/extensions/switchboard-local/node_modules/@valni/switchboard/dist/deviceAuth.js +155 -0
  152. package/runtime/extensions/switchboard-local/node_modules/@valni/switchboard/dist/errors.js +95 -0
  153. package/runtime/extensions/switchboard-local/node_modules/@valni/switchboard/dist/index.js +8 -0
  154. package/runtime/extensions/switchboard-local/node_modules/@valni/switchboard/dist/messages.js +253 -0
  155. package/runtime/extensions/switchboard-local/node_modules/@valni/switchboard/dist/models.js +19 -0
  156. package/runtime/extensions/switchboard-local/node_modules/@valni/switchboard/dist/native.js +886 -0
  157. package/runtime/extensions/switchboard-local/node_modules/@valni/switchboard/dist/wire.js +85 -0
  158. package/runtime/extensions/switchboard-local/node_modules/@valni/switchboard/package.json +39 -0
  159. package/runtime/extensions/switchboard-local/node_modules/@valni/switchboard-native/package.json +29 -0
  160. package/runtime/extensions/switchboard-local/node_modules/@valni/switchboard-native/src/anthropic.ts +127 -0
  161. package/runtime/extensions/switchboard-local/node_modules/@valni/switchboard-native/src/google.ts +111 -0
  162. package/runtime/extensions/switchboard-local/node_modules/@valni/switchboard-native/src/index.ts +8 -0
  163. package/runtime/extensions/switchboard-local/node_modules/@valni/switchboard-native/src/modelRecord.test.ts +25 -0
  164. package/runtime/extensions/switchboard-local/node_modules/@valni/switchboard-native/src/modelRecord.ts +57 -0
  165. package/runtime/extensions/switchboard-local/node_modules/@valni/switchboard-native/src/openaiGeneric.ts +103 -0
  166. package/runtime/extensions/switchboard-local/node_modules/@valni/switchboard-native/src/openaiPro.ts +89 -0
  167. package/runtime/extensions/switchboard-local/node_modules/@valni/switchboard-native/src/profile.ts +120 -0
  168. package/runtime/extensions/switchboard-local/node_modules/@valni/switchboard-native/src/routing.test.ts +82 -0
  169. package/runtime/extensions/switchboard-local/node_modules/@valni/switchboard-native/src/routing.ts +74 -0
  170. package/runtime/extensions/switchboard-local/node_modules/@valni/switchboard-native/src/validateConfig.google.test.ts +137 -0
  171. package/runtime/extensions/switchboard-local/node_modules/@valni/switchboard-native/src/validateConfig.openai.test.ts +241 -0
  172. package/runtime/extensions/switchboard-local/node_modules/@valni/switchboard-native/src/validateConfig.test.ts +174 -0
  173. package/runtime/extensions/switchboard-local/node_modules/@valni/switchboard-native/src/validateConfig.ts +438 -0
  174. package/runtime/extensions/switchboard-local/package.json +34 -0
  175. package/runtime/extensions/switchboard-local/tsconfig.json +13 -0
  176. package/runtime/native/darwin/prebuilds/darwin-x64/darwin-modifiers.node +0 -0
  177. package/runtime/package.json +98 -0
  178. package/runtime/photon_rs_bg.wasm +0 -0
  179. package/runtime/theme/dark.json +86 -0
  180. package/runtime/theme/light.json +85 -0
  181. package/runtime/theme/theme-schema.json +340 -0
  182. package/runtime/valni +4 -0
  183. package/README.md +0 -3
@@ -0,0 +1,324 @@
1
+ import { spawn } from "node:child_process";
2
+ import { chmodSync, existsSync, mkdirSync, rmSync } from "node:fs";
3
+ import { writeFile } from "node:fs/promises";
4
+ import { join } from "node:path";
5
+ import type { ChildProcess } from "node:child_process";
6
+ import type { LocalEngine, LocalModelInfo, Progress } from "../engine.ts";
7
+ import { llamaBinDir, llamaDir, llamaModelsDir } from "../paths.ts";
8
+ import { claimOwnership, freePort, isAlive, portIsFree, readState, releaseOwnership, updateState } from "../store.ts";
9
+ import { ggufQuants, repoFiles, type Quantization } from "../hf.ts";
10
+ import { buildPlan, downloadUrl, latestBuild, type BuildChoice } from "./llamaBuild.ts";
11
+ import { copyDir, detachChild, directorySize, findFile, healthy, killOnExit, killPid, run, stopChild, watchSpawn } from "./support.ts";
12
+
13
+ const SERVER_NAME = process.platform === "win32" ? "llama-server.exe" : "llama-server";
14
+ const HEALTH_TIMEOUT_MS = 180_000;
15
+ const HEALTH_POLL_MS = 500;
16
+ const DOWNLOAD_POLL_MS = 500;
17
+ const CONTEXT_PER_MODEL = 32_768;
18
+ const DEFAULT_CONTEXT_WINDOW = 8_192;
19
+
20
+ interface RouterModel {
21
+ id: string;
22
+ status: string;
23
+ contextWindow: number;
24
+ vision: boolean;
25
+ }
26
+
27
+ function parseRouterModels(payload: unknown): RouterModel[] {
28
+ const data = (payload as { data?: unknown }).data;
29
+ if (!Array.isArray(data)) return [];
30
+ return data.flatMap((value) => {
31
+ if (typeof value !== "object" || value === null) return [];
32
+ const model = value as {
33
+ id?: unknown;
34
+ status?: { value?: unknown };
35
+ meta?: { n_ctx?: unknown; n_ctx_train?: unknown };
36
+ architecture?: { input_modalities?: unknown };
37
+ };
38
+ if (typeof model.id !== "string") return [];
39
+ const reported = [model.meta?.n_ctx, model.meta?.n_ctx_train].find(
40
+ (candidate) => typeof candidate === "number" && candidate > 0,
41
+ );
42
+ const modalities = model.architecture?.input_modalities;
43
+ return [
44
+ {
45
+ id: model.id,
46
+ status: typeof model.status?.value === "string" ? model.status.value : "unknown",
47
+ contextWindow: typeof reported === "number" ? reported : DEFAULT_CONTEXT_WINDOW,
48
+ vision: Array.isArray(modalities) && modalities.includes("image"),
49
+ },
50
+ ];
51
+ });
52
+ }
53
+
54
+ export class GgufEngine implements LocalEngine {
55
+ readonly id = "gguf" as const;
56
+ readonly label = "GGUF";
57
+
58
+ child: ChildProcess | undefined;
59
+ private port: number | undefined;
60
+ private starting: Promise<void> | undefined;
61
+ private exitHookInstalled = false;
62
+ private adopted = false;
63
+
64
+ supported(): boolean {
65
+ return buildPlan(process.platform, process.arch) !== null;
66
+ }
67
+
68
+ unsupportedReason(): string {
69
+ return `llama.cpp ships no build for ${process.platform} ${process.arch}.`;
70
+ }
71
+
72
+ serverPath(): string {
73
+ return join(llamaBinDir(), SERVER_NAME);
74
+ }
75
+
76
+ isInstalled(): boolean {
77
+ return existsSync(this.serverPath());
78
+ }
79
+
80
+ isRunning(): boolean {
81
+ return this.port !== undefined;
82
+ }
83
+
84
+ baseUrl(): string {
85
+ if (this.port === undefined) throw new Error("the GGUF engine is not running");
86
+ return `http://127.0.0.1:${this.port}/v1`;
87
+ }
88
+
89
+ private routerUrl(path: string): string {
90
+ if (this.port === undefined) throw new Error("the GGUF engine is not running");
91
+ return `http://127.0.0.1:${this.port}${path}`;
92
+ }
93
+
94
+ async ensureReady(onProgress: (progress: Progress) => void, signal?: AbortSignal): Promise<void> {
95
+ if (!this.supported()) throw new Error(this.unsupportedReason());
96
+ if (this.port !== undefined) return;
97
+ this.starting ??= this.start(onProgress, signal).finally(() => {
98
+ this.starting = undefined;
99
+ });
100
+ await this.starting;
101
+ }
102
+
103
+ private async start(onProgress: (progress: Progress) => void, signal?: AbortSignal): Promise<void> {
104
+ const plan = buildPlan(process.platform, process.arch);
105
+ if (plan === null) throw new Error(this.unsupportedReason());
106
+ if (!this.isInstalled()) await this.install(plan.preferred, onProgress, signal);
107
+ const existing = readState().ggufPort;
108
+ if (existing !== undefined && (await healthy(`http://127.0.0.1:${existing}/health`))) {
109
+ this.port = existing;
110
+ this.adopted = true;
111
+ claimOwnership("ggufOwners", process.pid);
112
+ this.installExitHook();
113
+ return;
114
+ }
115
+ this.killStale();
116
+ try {
117
+ await this.launch(onProgress, signal);
118
+ } catch (error) {
119
+ if (plan.fallback === undefined) throw error;
120
+ onProgress({ label: `${plan.preferred.accelerator} build did not run, falling back to ${plan.fallback.accelerator}` });
121
+ rmSync(llamaBinDir(), { recursive: true, force: true });
122
+ await this.install(plan.fallback, onProgress, signal);
123
+ await this.launch(onProgress, signal);
124
+ }
125
+ }
126
+
127
+ private async install(
128
+ choice: BuildChoice,
129
+ onProgress: (progress: Progress) => void,
130
+ signal?: AbortSignal,
131
+ ): Promise<void> {
132
+ const build = readState().ggufBuild ?? (await latestBuild(signal));
133
+ onProgress({ label: `Downloading the llama.cpp ${choice.accelerator} build ${build}` });
134
+ const url = downloadUrl(build, choice);
135
+ const timeout = AbortSignal.timeout(600_000);
136
+ const response = await fetch(url, { signal: signal ? AbortSignal.any([signal, timeout]) : timeout });
137
+ if (!response.ok) throw new Error(`could not download ${url}: HTTP ${response.status}`);
138
+ mkdirSync(llamaDir(), { recursive: true });
139
+ const archive = join(llamaDir(), "download");
140
+ await writeFile(archive, Buffer.from(await response.arrayBuffer()));
141
+ onProgress({ label: "Unpacking the llama.cpp build" });
142
+ const staging = join(llamaDir(), "staging");
143
+ rmSync(staging, { recursive: true, force: true });
144
+ mkdirSync(staging, { recursive: true });
145
+ await run("tar", ["-xf", archive, "-C", staging], { signal });
146
+ rmSync(archive, { force: true });
147
+ const server = findFile(staging, SERVER_NAME);
148
+ if (server === undefined) throw new Error(`${SERVER_NAME} was not in the llama.cpp build`);
149
+ rmSync(llamaBinDir(), { recursive: true, force: true });
150
+ mkdirSync(llamaBinDir(), { recursive: true });
151
+ copyDir(join(server, ".."), llamaBinDir());
152
+ rmSync(staging, { recursive: true, force: true });
153
+ chmodSync(this.serverPath(), 0o755);
154
+ updateState({ ggufBuild: build, ggufAccelerator: choice.accelerator });
155
+ }
156
+
157
+ private async launch(onProgress: (progress: Progress) => void, signal?: AbortSignal): Promise<void> {
158
+ const existing = readState().ggufPort;
159
+ const port = existing !== undefined && (await portIsFree(existing)) ? existing : await freePort();
160
+ mkdirSync(llamaModelsDir(), { recursive: true });
161
+ onProgress({ label: `Starting the GGUF engine on port ${port}` });
162
+ const child = spawn(
163
+ this.serverPath(),
164
+ [
165
+ "--models-dir",
166
+ llamaModelsDir(),
167
+ "--jinja",
168
+ "--host",
169
+ "127.0.0.1",
170
+ "--port",
171
+ String(port),
172
+ "-ngl",
173
+ "999",
174
+ "-c",
175
+ String(CONTEXT_PER_MODEL),
176
+ ],
177
+ {
178
+ cwd: llamaDir(),
179
+ env: { ...process.env, LLAMA_CACHE: llamaModelsDir() },
180
+ detached: process.platform !== "win32",
181
+ stdio: ["ignore", "ignore", "pipe"],
182
+ },
183
+ );
184
+ let stderr = "";
185
+ child.stderr?.on("data", (chunk: Buffer) => {
186
+ stderr = `${stderr}${chunk.toString()}`.slice(-4000);
187
+ });
188
+ const spawnFailure = watchSpawn(child);
189
+ let exited = false;
190
+ child.once("exit", () => {
191
+ exited = true;
192
+ if (this.child === child) {
193
+ this.child = undefined;
194
+ this.port = undefined;
195
+ }
196
+ });
197
+ this.child = child;
198
+ detachChild(child);
199
+ updateState({ ggufPort: port, ggufPid: child.pid });
200
+ claimOwnership("ggufOwners", process.pid);
201
+ this.installExitHook();
202
+ const deadline = Date.now() + HEALTH_TIMEOUT_MS;
203
+ while (Date.now() < deadline) {
204
+ if (signal?.aborted) {
205
+ child.kill("SIGTERM");
206
+ throw new Error("cancelled");
207
+ }
208
+ if (exited) {
209
+ const failure = spawnFailure.failure();
210
+ if (failure !== undefined) throw failure;
211
+ const tail = stderr.trim().split("\n").slice(-4).join("\n");
212
+ throw new Error(`the GGUF engine stopped while starting${tail ? `\n${tail}` : ""}`);
213
+ }
214
+ if (await healthy(`http://127.0.0.1:${port}/health`)) {
215
+ this.port = port;
216
+ return;
217
+ }
218
+ await new Promise((resolve) => setTimeout(resolve, HEALTH_POLL_MS));
219
+ }
220
+ child.kill("SIGTERM");
221
+ throw new Error("the GGUF engine did not start in time");
222
+ }
223
+
224
+ private installExitHook(): void {
225
+ if (this.exitHookInstalled) return;
226
+ this.exitHookInstalled = true;
227
+ killOnExit(() => this.releaseAndMaybeKill());
228
+ }
229
+
230
+ private releaseAndMaybeKill(): number | undefined {
231
+ const remaining = releaseOwnership("ggufOwners", process.pid);
232
+ if (remaining.length > 0) return undefined;
233
+ const pid = this.child?.pid ?? readState().ggufPid;
234
+ if (pid === undefined || !isAlive(pid)) return undefined;
235
+ updateState({ ggufPid: undefined });
236
+ return pid;
237
+ }
238
+
239
+ private killStale(): void {
240
+ const pid = readState().ggufPid;
241
+ if (pid === undefined || pid === process.pid) return;
242
+ killPid(pid, "SIGTERM");
243
+ updateState({ ggufPid: undefined });
244
+ }
245
+
246
+ async addModel(
247
+ reference: string,
248
+ onProgress: (progress: Progress) => void,
249
+ signal?: AbortSignal,
250
+ ): Promise<void> {
251
+ const [repoId, requested] = reference.split(":");
252
+ onProgress({ label: `Looking up ${repoId}` });
253
+ const quants = ggufQuants(await repoFiles(repoId, signal));
254
+ if (quants.length === 0) throw new Error(`${repoId} has no GGUF weights`);
255
+ const chosen = this.chooseQuant(quants, requested);
256
+ const modelId = `${repoId}:${chosen.name}`;
257
+ const before = directorySize(llamaModelsDir());
258
+ const response = await fetch(this.routerUrl("/models"), {
259
+ method: "POST",
260
+ headers: { "Content-Type": "application/json" },
261
+ body: JSON.stringify({ model: modelId }),
262
+ signal,
263
+ });
264
+ if (!response.ok) throw new Error(`the GGUF engine refused ${modelId}: HTTP ${response.status}`);
265
+ while (true) {
266
+ if (signal?.aborted) throw new Error("cancelled");
267
+ const models = await this.routerModels(signal);
268
+ const model = models.find((candidate) => candidate.id === modelId);
269
+ if (model !== undefined && model.status !== "downloading") return;
270
+ const written = Math.max(0, directorySize(llamaModelsDir()) - before);
271
+ const fraction = chosen.size > 0 ? Math.min(1, written / chosen.size) : undefined;
272
+ onProgress({ label: `Downloading ${modelId}`, fraction });
273
+ await new Promise((resolve) => setTimeout(resolve, DOWNLOAD_POLL_MS));
274
+ }
275
+ }
276
+
277
+ private chooseQuant(quants: readonly Quantization[], requested: string | undefined): Quantization {
278
+ if (requested === undefined) return quants[0];
279
+ const wanted = requested.toUpperCase();
280
+ const match = quants.find((quant) => quant.name === wanted);
281
+ if (match === undefined) {
282
+ throw new Error(`that repository has no ${wanted}. Available: ${quants.map((q) => q.name).join(", ")}`);
283
+ }
284
+ return match;
285
+ }
286
+
287
+ private async routerModels(signal?: AbortSignal): Promise<RouterModel[]> {
288
+ const timeout = AbortSignal.timeout(10_000);
289
+ const response = await fetch(this.routerUrl("/models"), {
290
+ signal: signal ? AbortSignal.any([signal, timeout]) : timeout,
291
+ });
292
+ if (!response.ok) throw new Error(`the GGUF engine returned HTTP ${response.status} for its model list`);
293
+ return parseRouterModels(await response.json());
294
+ }
295
+
296
+ async listModels(signal?: AbortSignal): Promise<LocalModelInfo[]> {
297
+ if (this.port === undefined) return [];
298
+ const baseUrl = this.baseUrl();
299
+ return (await this.routerModels(signal))
300
+ .filter((model) => model.status !== "downloading")
301
+ .map((model) => ({
302
+ id: model.id,
303
+ engine: this.id,
304
+ baseUrl,
305
+ contextWindow: model.contextWindow,
306
+ vision: model.vision,
307
+ }));
308
+ }
309
+
310
+ async stop(): Promise<void> {
311
+ const child = this.child;
312
+ const adopted = this.adopted;
313
+ this.child = undefined;
314
+ this.port = undefined;
315
+ this.adopted = false;
316
+ const remaining = releaseOwnership("ggufOwners", process.pid);
317
+ if (remaining.length > 0 || adopted) return;
318
+ if (!child || child.exitCode !== null) return;
319
+ updateState({ ggufPid: undefined });
320
+ await stopChild(child, 5_000);
321
+ }
322
+ }
323
+
324
+ export { parseRouterModels };
@@ -0,0 +1,62 @@
1
+ export interface BuildChoice {
2
+ asset: string;
3
+ accelerator: string;
4
+ }
5
+
6
+ export interface BuildPlan {
7
+ preferred: BuildChoice;
8
+ fallback?: BuildChoice;
9
+ }
10
+
11
+ export function buildPlan(platform: NodeJS.Platform, arch: string): BuildPlan | null {
12
+ if (platform === "darwin" && arch === "arm64") {
13
+ return { preferred: { asset: "macos-arm64.tar.gz", accelerator: "Metal" } };
14
+ }
15
+ if (platform === "darwin" && arch === "x64") {
16
+ return { preferred: { asset: "macos-x64.tar.gz", accelerator: "Metal" } };
17
+ }
18
+ if (platform === "win32" && arch === "x64") {
19
+ return {
20
+ preferred: { asset: "win-vulkan-x64.zip", accelerator: "Vulkan" },
21
+ fallback: { asset: "win-cpu-x64.zip", accelerator: "CPU" },
22
+ };
23
+ }
24
+ if (platform === "win32" && arch === "arm64") {
25
+ return { preferred: { asset: "win-cpu-arm64.zip", accelerator: "CPU" } };
26
+ }
27
+ if (platform === "linux" && arch === "x64") {
28
+ return {
29
+ preferred: { asset: "ubuntu-vulkan-x64.tar.gz", accelerator: "Vulkan" },
30
+ fallback: { asset: "ubuntu-x64.tar.gz", accelerator: "CPU" },
31
+ };
32
+ }
33
+ if (platform === "linux" && arch === "arm64") {
34
+ return {
35
+ preferred: { asset: "ubuntu-vulkan-arm64.tar.gz", accelerator: "Vulkan" },
36
+ fallback: { asset: "ubuntu-arm64.tar.gz", accelerator: "CPU" },
37
+ };
38
+ }
39
+ return null;
40
+ }
41
+
42
+ export function assetName(build: string, choice: BuildChoice): string {
43
+ return `llama-${build}-bin-${choice.asset}`;
44
+ }
45
+
46
+ export async function latestBuild(signal?: AbortSignal): Promise<string> {
47
+ const timeout = AbortSignal.timeout(20_000);
48
+ const response = await fetch("https://api.github.com/repos/ggml-org/llama.cpp/releases?per_page=1", {
49
+ headers: { Accept: "application/vnd.github+json" },
50
+ signal: signal ? AbortSignal.any([signal, timeout]) : timeout,
51
+ });
52
+ if (!response.ok) throw new Error(`GitHub returned HTTP ${response.status} for llama.cpp releases`);
53
+ const payload: unknown = await response.json();
54
+ if (!Array.isArray(payload) || payload.length === 0) throw new Error("no llama.cpp releases were listed");
55
+ const tag = (payload[0] as { tag_name?: unknown }).tag_name;
56
+ if (typeof tag !== "string" || !tag) throw new Error("the newest llama.cpp release has no tag");
57
+ return tag;
58
+ }
59
+
60
+ export function downloadUrl(build: string, choice: BuildChoice): string {
61
+ return `https://github.com/ggml-org/llama.cpp/releases/download/${build}/${assetName(build, choice)}`;
62
+ }
@@ -0,0 +1,245 @@
1
+ import { spawn } from "node:child_process";
2
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import type { ChildProcess } from "node:child_process";
5
+ import type { LocalEngine, LocalModelInfo, Progress } from "../engine.ts";
6
+ import { managedDir, mlxVenvBin, mlxVenvDir, repoCacheDir } from "../paths.ts";
7
+ import { claimOwnership, freePort, isAlive, portIsFree, readState, releaseOwnership, updateState } from "../store.ts";
8
+ import { looksLikeMlxRepo, repoFiles, totalBytes } from "../hf.ts";
9
+ import { detachChild, directorySize, healthy, killOnExit, killPid, run, stopChild, watchSpawn } from "./support.ts";
10
+
11
+ const HEALTH_TIMEOUT_MS = 120_000;
12
+ const HEALTH_POLL_MS = 250;
13
+ const DOWNLOAD_POLL_MS = 500;
14
+ const DEFAULT_CONTEXT_WINDOW = 32_768;
15
+
16
+ function findContextWindow(value: unknown): number | undefined {
17
+ if (typeof value !== "object" || value === null) return undefined;
18
+ const record = value as Record<string, unknown>;
19
+ const direct = record.max_position_embeddings;
20
+ if (typeof direct === "number" && direct > 0) return direct;
21
+ for (const nested of Object.values(record)) {
22
+ const found = findContextWindow(nested);
23
+ if (found !== undefined) return found;
24
+ }
25
+ return undefined;
26
+ }
27
+
28
+ function contextWindowFor(repoId: string): number {
29
+ const snapshots = join(repoCacheDir(repoId), "snapshots");
30
+ let revisions: string[];
31
+ try {
32
+ revisions = readdirSync(snapshots);
33
+ } catch {
34
+ return DEFAULT_CONTEXT_WINDOW;
35
+ }
36
+ for (const revision of revisions) {
37
+ try {
38
+ const raw = readFileSync(join(snapshots, revision, "config.json"), "utf8");
39
+ const found = findContextWindow(JSON.parse(raw));
40
+ if (found !== undefined) return found;
41
+ } catch {
42
+ continue;
43
+ }
44
+ }
45
+ return DEFAULT_CONTEXT_WINDOW;
46
+ }
47
+
48
+ export class MlxEngine implements LocalEngine {
49
+ readonly id = "mlx" as const;
50
+ readonly label = "MLX";
51
+
52
+ child: ChildProcess | undefined;
53
+ private port: number | undefined;
54
+ private starting: Promise<void> | undefined;
55
+ private exitHookInstalled = false;
56
+ private adopted = false;
57
+
58
+ supported(): boolean {
59
+ return process.platform === "darwin" && process.arch === "arm64";
60
+ }
61
+
62
+ unsupportedReason(): string {
63
+ return "MLX runs on Apple Silicon only. Support for other machines arrives with the GGUF engine.";
64
+ }
65
+
66
+ isInstalled(): boolean {
67
+ return existsSync(mlxVenvBin("mlx_lm.server"));
68
+ }
69
+
70
+ isRunning(): boolean {
71
+ return this.port !== undefined;
72
+ }
73
+
74
+ baseUrl(): string {
75
+ if (this.port === undefined) throw new Error("the MLX engine is not running");
76
+ return `http://127.0.0.1:${this.port}/v1`;
77
+ }
78
+
79
+ async ensureReady(onProgress: (progress: Progress) => void, signal?: AbortSignal): Promise<void> {
80
+ if (!this.supported()) throw new Error(this.unsupportedReason());
81
+ if (this.port !== undefined) return;
82
+ this.starting ??= this.start(onProgress, signal).finally(() => {
83
+ this.starting = undefined;
84
+ });
85
+ await this.starting;
86
+ }
87
+
88
+ private async start(onProgress: (progress: Progress) => void, signal?: AbortSignal): Promise<void> {
89
+ await this.install(onProgress, signal);
90
+ const existing = readState().mlxPort;
91
+ if (existing !== undefined && (await healthy(`http://127.0.0.1:${existing}/health`))) {
92
+ this.port = existing;
93
+ this.adopted = true;
94
+ claimOwnership("mlxOwners", process.pid);
95
+ this.installExitHook();
96
+ return;
97
+ }
98
+ this.killStale();
99
+ const port = existing !== undefined && (await portIsFree(existing)) ? existing : await freePort();
100
+ onProgress({ label: `Starting the MLX engine on port ${port}` });
101
+ const child = spawn(mlxVenvBin("mlx_lm.server"), ["--host", "127.0.0.1", "--port", String(port)], {
102
+ cwd: managedDir(),
103
+ detached: process.platform !== "win32",
104
+ stdio: ["ignore", "ignore", "pipe"],
105
+ });
106
+ let stderr = "";
107
+ child.stderr?.on("data", (chunk: Buffer) => {
108
+ stderr = `${stderr}${chunk.toString()}`.slice(-4000);
109
+ });
110
+ const spawnFailure = watchSpawn(child);
111
+ let exited = false;
112
+ child.once("exit", () => {
113
+ exited = true;
114
+ if (this.child === child) {
115
+ this.child = undefined;
116
+ this.port = undefined;
117
+ }
118
+ });
119
+ this.child = child;
120
+ detachChild(child);
121
+ updateState({ mlxPort: port, mlxPid: child.pid });
122
+ claimOwnership("mlxOwners", process.pid);
123
+ this.installExitHook();
124
+ const deadline = Date.now() + HEALTH_TIMEOUT_MS;
125
+ while (Date.now() < deadline) {
126
+ if (signal?.aborted) {
127
+ child.kill("SIGTERM");
128
+ throw new Error("cancelled");
129
+ }
130
+ if (exited) {
131
+ const failure = spawnFailure.failure();
132
+ if (failure !== undefined) throw failure;
133
+ const tail = stderr.trim().split("\n").slice(-4).join("\n");
134
+ throw new Error(`the MLX engine stopped while starting${tail ? `\n${tail}` : ""}`);
135
+ }
136
+ if (await healthy(`http://127.0.0.1:${port}/health`)) {
137
+ this.port = port;
138
+ return;
139
+ }
140
+ await new Promise((resolve) => setTimeout(resolve, HEALTH_POLL_MS));
141
+ }
142
+ child.kill("SIGTERM");
143
+ throw new Error("the MLX engine did not start in time");
144
+ }
145
+
146
+ private async install(onProgress: (progress: Progress) => void, signal?: AbortSignal): Promise<void> {
147
+ if (this.isInstalled()) return;
148
+ if (!existsSync(mlxVenvBin("python"))) {
149
+ onProgress({ label: "Creating the Python environment" });
150
+ await run("python3", ["-m", "venv", mlxVenvDir()], { signal });
151
+ }
152
+ onProgress({ label: "Installing mlx-lm, this happens once" });
153
+ await run(mlxVenvBin("pip"), ["install", "--quiet", "--upgrade", "mlx-lm"], { signal });
154
+ if (!this.isInstalled()) throw new Error("mlx-lm installed but mlx_lm.server is missing");
155
+ }
156
+
157
+ private releaseAndMaybeKill(): number | undefined {
158
+ const remaining = releaseOwnership("mlxOwners", process.pid);
159
+ if (remaining.length > 0) return undefined;
160
+ const pid = this.child?.pid ?? readState().mlxPid;
161
+ if (pid === undefined || !isAlive(pid)) return undefined;
162
+ updateState({ mlxPid: undefined });
163
+ return pid;
164
+ }
165
+
166
+ private killStale(): void {
167
+ const pid = readState().mlxPid;
168
+ if (pid === undefined || pid === process.pid) return;
169
+ killPid(pid, "SIGTERM");
170
+ updateState({ mlxPid: undefined });
171
+ }
172
+
173
+ private installExitHook(): void {
174
+ if (this.exitHookInstalled) return;
175
+ this.exitHookInstalled = true;
176
+ killOnExit(() => this.releaseAndMaybeKill());
177
+ }
178
+
179
+ async addModel(
180
+ repoId: string,
181
+ onProgress: (progress: Progress) => void,
182
+ signal?: AbortSignal,
183
+ ): Promise<void> {
184
+ if (!this.supported()) throw new Error(this.unsupportedReason());
185
+ onProgress({ label: `Looking up ${repoId}` });
186
+ const files = await repoFiles(repoId, signal);
187
+ if (!looksLikeMlxRepo(files)) {
188
+ throw new Error(
189
+ `${repoId} does not look like an MLX model. Look for a repository with safetensors weights, such as one under mlx-community.`,
190
+ );
191
+ }
192
+ const total = totalBytes(files);
193
+ const target = repoCacheDir(repoId);
194
+ const already = directorySize(target);
195
+ const download = run(
196
+ mlxVenvBin("python"),
197
+ ["-c", "import sys;from huggingface_hub import snapshot_download;snapshot_download(sys.argv[1])", repoId],
198
+ { signal },
199
+ );
200
+ let finished = false;
201
+ void download.finally(() => {
202
+ finished = true;
203
+ });
204
+ while (!finished) {
205
+ const current = directorySize(target);
206
+ const fraction = total > 0 ? Math.min(1, current / total) : undefined;
207
+ onProgress({ label: `Downloading ${repoId}`, fraction });
208
+ await new Promise((resolve) => setTimeout(resolve, DOWNLOAD_POLL_MS));
209
+ }
210
+ await download;
211
+ if (already === 0 && directorySize(target) === 0) throw new Error(`nothing was downloaded for ${repoId}`);
212
+ }
213
+
214
+ async listModels(signal?: AbortSignal): Promise<LocalModelInfo[]> {
215
+ if (this.port === undefined) return [];
216
+ const timeout = AbortSignal.timeout(10_000);
217
+ const response = await fetch(`http://127.0.0.1:${this.port}/v1/models`, {
218
+ signal: signal ? AbortSignal.any([signal, timeout]) : timeout,
219
+ });
220
+ if (!response.ok) throw new Error(`the MLX engine returned HTTP ${response.status} for its model list`);
221
+ const payload: unknown = await response.json();
222
+ const data = (payload as { data?: unknown }).data;
223
+ if (!Array.isArray(data)) return [];
224
+ const baseUrl = this.baseUrl();
225
+ return data.flatMap((value) => {
226
+ if (typeof value !== "object" || value === null) return [];
227
+ const id = (value as { id?: unknown }).id;
228
+ if (typeof id !== "string" || !id.includes("/")) return [];
229
+ return [{ id, engine: this.id, baseUrl, contextWindow: contextWindowFor(id) }];
230
+ });
231
+ }
232
+
233
+ async stop(): Promise<void> {
234
+ const child = this.child;
235
+ const adopted = this.adopted;
236
+ this.child = undefined;
237
+ this.port = undefined;
238
+ this.adopted = false;
239
+ const remaining = releaseOwnership("mlxOwners", process.pid);
240
+ if (remaining.length > 0 || adopted) return;
241
+ if (!child || child.exitCode !== null) return;
242
+ updateState({ mlxPid: undefined });
243
+ await stopChild(child, 3_000);
244
+ }
245
+ }
@@ -0,0 +1,38 @@
1
+ import type { LocalEngine } from "../engine.ts";
2
+ import { ggufQuants, looksLikeMlxRepo, parseModelRef, repoFiles } from "../hf.ts";
3
+
4
+ export interface EngineChoice {
5
+ engine: LocalEngine;
6
+ reference: string;
7
+ }
8
+
9
+ export async function chooseEngine(
10
+ engines: readonly LocalEngine[],
11
+ input: string,
12
+ signal?: AbortSignal,
13
+ ): Promise<EngineChoice> {
14
+ const ref = parseModelRef(input);
15
+ if (ref === null) {
16
+ throw new Error(`"${input}" is not a Hugging Face model. Use a link, or owner/repository[:quant].`);
17
+ }
18
+ const byId = (id: string) => engines.find((engine) => engine.id === id);
19
+ if (ref.quant !== undefined) {
20
+ return { engine: usable(byId("gguf"), "GGUF"), reference: `${ref.repoId}:${ref.quant}` };
21
+ }
22
+ const files = await repoFiles(ref.repoId, signal);
23
+ if (ggufQuants(files).length > 0) {
24
+ return { engine: usable(byId("gguf"), "GGUF"), reference: ref.repoId };
25
+ }
26
+ if (looksLikeMlxRepo(files)) {
27
+ return { engine: usable(byId("mlx"), "MLX"), reference: ref.repoId };
28
+ }
29
+ throw new Error(
30
+ `${ref.repoId} has neither GGUF weights nor MLX safetensors, so nothing here can run it.`,
31
+ );
32
+ }
33
+
34
+ function usable(engine: LocalEngine | undefined, label: string): LocalEngine {
35
+ if (engine === undefined) throw new Error(`the ${label} engine is not available`);
36
+ if (!engine.supported()) throw new Error(engine.unsupportedReason());
37
+ return engine;
38
+ }