@gridlock-compute/native-worker 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,223 @@
1
+ import { BENCHMARK_TOKENS, INFERENCE_BACKEND, MAX_OUTPUT_TOKENS, OLLAMA_MODEL, OLLAMA_URL, OLLAMA_URL_CANDIDATES, VLLM_BASE_URL, VLLM_MODEL, setOllamaUrl, } from "./config.js";
2
+ import { bootstrapOllama, checkOllamaAt, findOllamaBinary, resolveOllamaModel, } from "./ollama.js";
3
+ import { Worker } from "node:worker_threads";
4
+ import { fileURLToPath } from "node:url";
5
+ import { dirname, join } from "node:path";
6
+ let activeBackend = null;
7
+ let activeModel = "";
8
+ export function getActiveBackend() {
9
+ if (!activeBackend)
10
+ throw new Error("Inference backend not initialized");
11
+ return activeBackend;
12
+ }
13
+ export function getActiveModel() {
14
+ return activeModel;
15
+ }
16
+ export async function checkOllama() {
17
+ if (process.env.GRIDLOCK_OLLAMA_URL) {
18
+ return checkOllamaAt(OLLAMA_URL);
19
+ }
20
+ for (const url of OLLAMA_URL_CANDIDATES) {
21
+ if (await checkOllamaAt(url)) {
22
+ setOllamaUrl(url);
23
+ return true;
24
+ }
25
+ }
26
+ return false;
27
+ }
28
+ async function ensureOllamaReady(modelPreference) {
29
+ if (!(await checkOllama())) {
30
+ const preferredUrl = process.env.GRIDLOCK_OLLAMA_URL?.replace(/\/$/, "") ?? OLLAMA_URL_CANDIDATES[0] ?? OLLAMA_URL;
31
+ setOllamaUrl(preferredUrl);
32
+ return bootstrapOllama(OLLAMA_URL, { preferred: modelPreference });
33
+ }
34
+ const binary = findOllamaBinary();
35
+ if (!binary) {
36
+ return bootstrapOllama(OLLAMA_URL, { preferred: modelPreference });
37
+ }
38
+ return resolveOllamaModel(OLLAMA_URL, binary, { preferred: modelPreference });
39
+ }
40
+ export async function checkVllm() {
41
+ try {
42
+ const res = await fetch(`${VLLM_BASE_URL}/models`, { signal: AbortSignal.timeout(5000) });
43
+ return res.ok;
44
+ }
45
+ catch {
46
+ return false;
47
+ }
48
+ }
49
+ export async function resolveInferenceBackend(preferred = INFERENCE_BACKEND, modelPreference) {
50
+ if (preferred === "ollama") {
51
+ activeModel = await ensureOllamaReady(modelPreference);
52
+ activeBackend = "ollama";
53
+ return activeBackend;
54
+ }
55
+ if (preferred === "vllm") {
56
+ if (!(await checkVllm())) {
57
+ throw new Error(`vLLM not reachable at ${VLLM_BASE_URL}. Start it with: vllm serve meta-llama/Llama-3.1-8B-Instruct --port 8000`);
58
+ }
59
+ activeBackend = "vllm";
60
+ activeModel = VLLM_MODEL;
61
+ return activeBackend;
62
+ }
63
+ // auto — prefer Ollama (installs on Linux/macOS if missing), fall back to vLLM
64
+ if (await checkOllama()) {
65
+ activeModel = await ensureOllamaReady(modelPreference);
66
+ activeBackend = "ollama";
67
+ return activeBackend;
68
+ }
69
+ try {
70
+ activeModel = await ensureOllamaReady(modelPreference);
71
+ activeBackend = "ollama";
72
+ return activeBackend;
73
+ }
74
+ catch (ollamaError) {
75
+ if (preferred !== "auto")
76
+ throw ollamaError;
77
+ }
78
+ if (await checkVllm()) {
79
+ activeBackend = "vllm";
80
+ activeModel = VLLM_MODEL;
81
+ return activeBackend;
82
+ }
83
+ throw new Error("No inference server found.\n" +
84
+ ` • Ollama: installs automatically on Linux/macOS, or https://ollama.com/download on Windows\n` +
85
+ ` • vLLM (Linux): vllm serve meta-llama/Llama-3.1-8B-Instruct --port 8000\n` +
86
+ "Set GRIDLOCK_INFERENCE=ollama|vllm to force one backend.");
87
+ }
88
+ async function runOllamaInference(messages, maxTokens, onProgress) {
89
+ const model = activeModel || OLLAMA_MODEL;
90
+ const workerPath = join(dirname(fileURLToPath(import.meta.url)), "inference-ollama-worker.js");
91
+ return new Promise((resolve, reject) => {
92
+ const worker = new Worker(workerPath);
93
+ let settled = false;
94
+ const finish = (fn) => {
95
+ if (settled)
96
+ return;
97
+ settled = true;
98
+ void worker.terminate();
99
+ fn();
100
+ };
101
+ worker.on("message", (msg) => {
102
+ if (msg.type === "progress") {
103
+ onProgress?.({
104
+ tokens: Number(msg.tokens ?? 0),
105
+ maxTokens: Number(msg.maxTokens ?? maxTokens),
106
+ });
107
+ return;
108
+ }
109
+ if (msg.type === "done" && msg.result) {
110
+ finish(() => resolve(msg.result));
111
+ return;
112
+ }
113
+ if (msg.type === "error") {
114
+ finish(() => reject(new Error(msg.error ?? "Ollama worker failed")));
115
+ }
116
+ });
117
+ worker.on("error", (err) => {
118
+ finish(() => reject(err));
119
+ });
120
+ worker.on("exit", (code) => {
121
+ if (!settled && code !== 0) {
122
+ finish(() => reject(new Error(`Ollama worker exited (${code ?? "unknown"})`)));
123
+ }
124
+ });
125
+ worker.postMessage({
126
+ url: OLLAMA_URL,
127
+ model,
128
+ messages,
129
+ maxTokens,
130
+ });
131
+ });
132
+ }
133
+ async function runVllmInference(messages, maxTokens, onProgress) {
134
+ const start = performance.now();
135
+ let firstTokenAt = null;
136
+ let content = "";
137
+ let tokens = 0;
138
+ const res = await fetch(`${VLLM_BASE_URL}/chat/completions`, {
139
+ method: "POST",
140
+ headers: { "Content-Type": "application/json" },
141
+ body: JSON.stringify({
142
+ model: VLLM_MODEL,
143
+ messages,
144
+ max_tokens: maxTokens,
145
+ temperature: 0.7,
146
+ stream: true,
147
+ }),
148
+ });
149
+ if (!res.ok) {
150
+ const text = await res.text();
151
+ throw new Error(`vLLM error ${res.status}: ${text.slice(0, 200)}`);
152
+ }
153
+ if (!res.body)
154
+ throw new Error("vLLM returned empty body");
155
+ const reader = res.body.getReader();
156
+ const decoder = new TextDecoder();
157
+ let buffer = "";
158
+ while (true) {
159
+ const { done, value } = await reader.read();
160
+ if (done)
161
+ break;
162
+ buffer += decoder.decode(value, { stream: true });
163
+ const lines = buffer.split("\n");
164
+ buffer = lines.pop() ?? "";
165
+ for (const line of lines) {
166
+ const trimmed = line.trim();
167
+ if (!trimmed.startsWith("data:"))
168
+ continue;
169
+ const payload = trimmed.slice(5).trim();
170
+ if (payload === "[DONE]")
171
+ continue;
172
+ try {
173
+ const chunk = JSON.parse(payload);
174
+ const piece = chunk.choices?.[0]?.delta?.content ?? "";
175
+ if (piece) {
176
+ if (firstTokenAt === null)
177
+ firstTokenAt = performance.now();
178
+ content += piece;
179
+ tokens = Math.max(tokens, Math.ceil(content.length / 4));
180
+ onProgress?.({ tokens, maxTokens });
181
+ }
182
+ const usageTokens = Number(chunk.usage?.completion_tokens ?? 0);
183
+ if (usageTokens > 0) {
184
+ tokens = usageTokens;
185
+ onProgress?.({ tokens, maxTokens });
186
+ }
187
+ }
188
+ catch {
189
+ /* skip */
190
+ }
191
+ }
192
+ }
193
+ const end = performance.now();
194
+ const ttftMs = Math.floor((firstTokenAt ?? end) - start);
195
+ const outputTokens = Math.max(tokens, 1);
196
+ const tpotMs = outputTokens > 1 && firstTokenAt
197
+ ? Math.floor((end - firstTokenAt) / (outputTokens - 1))
198
+ : 0;
199
+ return { content: content.trim() || "(empty)", tokens: outputTokens, ttftMs, tpotMs };
200
+ }
201
+ export async function runInference(messages, maxTokensOrOptions = MAX_OUTPUT_TOKENS, maybeOptions) {
202
+ let maxTokens = MAX_OUTPUT_TOKENS;
203
+ let options;
204
+ if (typeof maxTokensOrOptions === "number") {
205
+ maxTokens = maxTokensOrOptions;
206
+ options = maybeOptions;
207
+ }
208
+ else {
209
+ options = maxTokensOrOptions;
210
+ maxTokens = options.maxTokens ?? MAX_OUTPUT_TOKENS;
211
+ }
212
+ const backend = getActiveBackend();
213
+ const onProgress = options?.onProgress;
214
+ return backend === "ollama"
215
+ ? runOllamaInference(messages, maxTokens, onProgress)
216
+ : runVllmInference(messages, maxTokens, onProgress);
217
+ }
218
+ export async function runBenchmark() {
219
+ const start = performance.now();
220
+ const result = await runInference([{ role: "user", content: "Say hi in one word." }], BENCHMARK_TOKENS);
221
+ const elapsedSec = Math.max((performance.now() - start) / 1000, 0.001);
222
+ return Math.round((result.tokens / elapsedSec) * 10) / 10;
223
+ }
@@ -0,0 +1,14 @@
1
+ import type { WorkerDashboard } from "./dashboard.js";
2
+ export declare const LIVE_STATS_POLL_MS = 3000;
3
+ export interface LiveStats {
4
+ jobsToday: number;
5
+ tokensToday: number;
6
+ jobsFailedToday: number;
7
+ workerStatus: string;
8
+ wsTokPerSec: number;
9
+ queueJobs: number;
10
+ inflightJobs: number;
11
+ workerInflightJobs: number;
12
+ }
13
+ export declare function fetchLiveStats(wallet: string, backendUrl: string): Promise<LiveStats | null>;
14
+ export declare function startLiveStatsPoll(wallet: string, backendUrl: string, dashboard: WorkerDashboard, isRunning: () => boolean): () => void;
@@ -0,0 +1,66 @@
1
+ export const LIVE_STATS_POLL_MS = 3_000;
2
+ async function fetchWorkerLiveStats(wallet, backendUrl) {
3
+ try {
4
+ const res = await fetch(`${backendUrl.replace(/\/$/, "")}/v1/workers/${wallet}`);
5
+ if (!res.ok)
6
+ return null;
7
+ const data = (await res.json());
8
+ const recentJobs = Array.isArray(data.recent_jobs) ? data.recent_jobs : [];
9
+ const dayStart = Date.now() / 1000 - 86_400;
10
+ const recentToday = recentJobs.filter((job) => Number(job.ts ?? 0) >= dayStart);
11
+ const fallbackTokensToday = recentToday.reduce((sum, job) => sum + Number(job.completion_tokens ?? job.tokens_generated ?? job.output_tokens ?? 0), 0);
12
+ const fallbackFailedToday = recentToday.filter((job) => job.status === "failed" || job.sla_met === false).length;
13
+ return {
14
+ jobsToday: Number(data.jobs_today ?? recentToday.length),
15
+ tokensToday: Number(data.tokens_today ?? fallbackTokensToday),
16
+ jobsFailedToday: Number(data.jobs_failed_today ?? fallbackFailedToday),
17
+ workerStatus: data.status ?? "Active",
18
+ wsTokPerSec: Number(data.ws_tok_per_sec ?? 0),
19
+ workerInflightJobs: Number(data.in_flight ?? 0),
20
+ };
21
+ }
22
+ catch {
23
+ return null;
24
+ }
25
+ }
26
+ async function fetchRouterQueueStats(backendUrl) {
27
+ try {
28
+ const res = await fetch(`${backendUrl.replace(/\/$/, "")}/v1/stats/ws`);
29
+ if (!res.ok)
30
+ return null;
31
+ const data = (await res.json());
32
+ return {
33
+ queueJobs: Number(data.jobs_in_queue ?? 0),
34
+ inflightJobs: Number(data.jobs_inflight ?? 0),
35
+ };
36
+ }
37
+ catch {
38
+ return null;
39
+ }
40
+ }
41
+ export async function fetchLiveStats(wallet, backendUrl) {
42
+ const [worker, queue] = await Promise.all([
43
+ fetchWorkerLiveStats(wallet, backendUrl),
44
+ fetchRouterQueueStats(backendUrl),
45
+ ]);
46
+ if (!worker)
47
+ return null;
48
+ return {
49
+ ...worker,
50
+ queueJobs: queue?.queueJobs ?? 0,
51
+ inflightJobs: queue?.inflightJobs ?? worker.workerInflightJobs,
52
+ };
53
+ }
54
+ export function startLiveStatsPoll(wallet, backendUrl, dashboard, isRunning) {
55
+ const poll = async () => {
56
+ if (!isRunning())
57
+ return;
58
+ const stats = await fetchLiveStats(wallet, backendUrl);
59
+ if (!stats || !isRunning())
60
+ return;
61
+ dashboard.applyLiveStats(stats);
62
+ };
63
+ void poll();
64
+ const timer = setInterval(() => void poll(), LIVE_STATS_POLL_MS);
65
+ return () => clearInterval(timer);
66
+ }
package/dist/lock.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ /** One worker process per wallet — prevents reconnect fights on the router. */
2
+ export declare function acquireWorkerLock(wallet: string): () => void;
package/dist/lock.js ADDED
@@ -0,0 +1,46 @@
1
+ import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ function lockPath(wallet) {
5
+ const dir = join(homedir(), ".gridlock");
6
+ mkdirSync(dir, { recursive: true });
7
+ return join(dir, `worker-${wallet.toLowerCase()}.lock`);
8
+ }
9
+ function pidAlive(pid) {
10
+ try {
11
+ process.kill(pid, 0);
12
+ return true;
13
+ }
14
+ catch {
15
+ return false;
16
+ }
17
+ }
18
+ /** One worker process per wallet — prevents reconnect fights on the router. */
19
+ export function acquireWorkerLock(wallet) {
20
+ const path = lockPath(wallet);
21
+ if (existsSync(path)) {
22
+ const raw = readFileSync(path, "utf8").trim();
23
+ const pid = Number.parseInt(raw, 10);
24
+ if (Number.isFinite(pid) && pid !== process.pid && pidAlive(pid)) {
25
+ throw new Error(`Another worker is already running for this wallet (PID ${pid}).\n` +
26
+ ` Stop it first: kill ${pid}`);
27
+ }
28
+ try {
29
+ unlinkSync(path);
30
+ }
31
+ catch {
32
+ /* stale */
33
+ }
34
+ }
35
+ writeFileSync(path, String(process.pid), "utf8");
36
+ return () => {
37
+ try {
38
+ if (existsSync(path) && readFileSync(path, "utf8").trim() === String(process.pid)) {
39
+ unlinkSync(path);
40
+ }
41
+ }
42
+ catch {
43
+ /* ignore */
44
+ }
45
+ };
46
+ }
@@ -0,0 +1,3 @@
1
+ import type { OllamaModelInfo } from "./ollama.js";
2
+ export declare function isInteractiveTerminal(): boolean;
3
+ export declare function promptOllamaModel(models: OllamaModelInfo[], preferred: string): Promise<string>;
@@ -0,0 +1,39 @@
1
+ import { createInterface } from "node:readline/promises";
2
+ import { stdin as input, stdout as output } from "node:process";
3
+ function formatSizeGb(bytes) {
4
+ if (!bytes || bytes <= 0)
5
+ return "—";
6
+ return `${(bytes / 1e9).toFixed(1)} GB`;
7
+ }
8
+ export function isInteractiveTerminal() {
9
+ return Boolean(process.stdin.isTTY && process.stdout.isTTY);
10
+ }
11
+ export async function promptOllamaModel(models, preferred) {
12
+ if (models.length === 0)
13
+ return preferred;
14
+ console.log("\nInstalled Ollama models:\n");
15
+ for (let i = 0; i < models.length; i += 1) {
16
+ const m = models[i];
17
+ console.log(` ${i + 1}. ${m.name} (${formatSizeGb(m.size)})`);
18
+ }
19
+ const pullOption = models.length + 1;
20
+ console.log(` ${pullOption}. Pull ${preferred} (default — not installed yet)\n`);
21
+ const rl = createInterface({ input, output });
22
+ try {
23
+ while (true) {
24
+ const answer = (await rl.question(`Select model [1-${pullOption}, default 1]: `)).trim();
25
+ if (!answer)
26
+ return models[0].name;
27
+ const choice = Number.parseInt(answer, 10);
28
+ if (Number.isFinite(choice) && choice >= 1 && choice <= models.length) {
29
+ return models[choice - 1].name;
30
+ }
31
+ if (choice === pullOption)
32
+ return preferred;
33
+ console.log(`Enter a number between 1 and ${pullOption}.`);
34
+ }
35
+ }
36
+ finally {
37
+ rl.close();
38
+ }
39
+ }
@@ -0,0 +1,22 @@
1
+ export declare function checkOllamaAt(baseUrl: string): Promise<boolean>;
2
+ export declare function findOllamaBinary(): string | null;
3
+ /** Install Ollama on Linux/macOS when missing (official install script). */
4
+ export declare function installOllama(): Promise<void>;
5
+ export declare function resolveOllamaBinary(): Promise<string>;
6
+ export declare function startOllamaServe(binary: string): void;
7
+ export declare function waitForOllama(baseUrl: string, timeoutMs?: number): Promise<boolean>;
8
+ export interface OllamaModelInfo {
9
+ name: string;
10
+ size: number;
11
+ }
12
+ export declare function listOllamaModels(baseUrl: string): Promise<OllamaModelInfo[]>;
13
+ export declare function findModelMatch(models: OllamaModelInfo[], preferred: string): string | null;
14
+ export declare function pickInstalledFallback(models: OllamaModelInfo[], preferred: string): string | null;
15
+ export declare function ensureOllamaModel(baseUrl: string, binary: string, model: string): Promise<void>;
16
+ export interface ResolveOllamaModelOptions {
17
+ /** User-requested model (--model); pulls if missing instead of picking another install. */
18
+ preferred?: string;
19
+ interactive?: boolean;
20
+ }
21
+ export declare function resolveOllamaModel(baseUrl: string, binary: string, options?: ResolveOllamaModelOptions): Promise<string>;
22
+ export declare function bootstrapOllama(baseUrl: string, modelOptions?: ResolveOllamaModelOptions): Promise<string>;
package/dist/ollama.js ADDED
@@ -0,0 +1,195 @@
1
+ import { spawn, spawnSync } from "node:child_process";
2
+ import { existsSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { workerLog } from "./console-io.js";
5
+ import { isInteractiveTerminal, promptOllamaModel } from "./model-select.js";
6
+ function sleep(ms) {
7
+ return new Promise((resolve) => setTimeout(resolve, ms));
8
+ }
9
+ export async function checkOllamaAt(baseUrl) {
10
+ try {
11
+ const res = await fetch(`${baseUrl}/api/version`, { signal: AbortSignal.timeout(5000) });
12
+ return res.ok;
13
+ }
14
+ catch {
15
+ return false;
16
+ }
17
+ }
18
+ export function findOllamaBinary() {
19
+ const candidates = [];
20
+ if (process.env.GRIDLOCK_OLLAMA_BIN) {
21
+ candidates.push(process.env.GRIDLOCK_OLLAMA_BIN);
22
+ }
23
+ if (process.platform === "win32") {
24
+ if (process.env.LOCALAPPDATA) {
25
+ candidates.push(join(process.env.LOCALAPPDATA, "Programs", "Ollama", "ollama.exe"));
26
+ }
27
+ if (process.env.ProgramFiles) {
28
+ candidates.push(join(process.env.ProgramFiles, "Ollama", "ollama.exe"));
29
+ }
30
+ }
31
+ candidates.push("ollama");
32
+ for (const candidate of candidates) {
33
+ if (candidate.includes("/") || candidate.includes("\\")) {
34
+ if (existsSync(candidate))
35
+ return candidate;
36
+ continue;
37
+ }
38
+ try {
39
+ const cmd = process.platform === "win32" ? "where" : "which";
40
+ const out = spawnSync(cmd, [candidate], { encoding: "utf8" });
41
+ if (out.status === 0 && out.stdout.trim()) {
42
+ return out.stdout.trim().split(/\r?\n/)[0] ?? candidate;
43
+ }
44
+ }
45
+ catch {
46
+ /* try next */
47
+ }
48
+ }
49
+ return null;
50
+ }
51
+ /** Install Ollama on Linux/macOS when missing (official install script). */
52
+ export async function installOllama() {
53
+ if (process.platform === "win32") {
54
+ throw new Error("Ollama is not installed.\n" +
55
+ " 1. Download from https://ollama.com/download\n" +
56
+ " 2. Install, then open Ollama from the Start menu\n" +
57
+ " 3. Re-run this worker");
58
+ }
59
+ if (process.env.GRIDLOCK_SKIP_OLLAMA_INSTALL === "true") {
60
+ throw new Error("Ollama is not installed. Set GRIDLOCK_SKIP_OLLAMA_INSTALL=false or install from https://ollama.com/download");
61
+ }
62
+ workerLog("Ollama not found — installing via https://ollama.com/install.sh …");
63
+ const result = spawnSync("sh", ["-c", "curl -fsSL https://ollama.com/install.sh | sh"], {
64
+ stdio: "inherit",
65
+ });
66
+ if (result.status !== 0) {
67
+ throw new Error("Ollama install failed. Install manually: curl -fsSL https://ollama.com/install.sh | sh");
68
+ }
69
+ }
70
+ export async function resolveOllamaBinary() {
71
+ const existing = findOllamaBinary();
72
+ if (existing)
73
+ return existing;
74
+ await installOllama();
75
+ const binary = findOllamaBinary();
76
+ if (!binary) {
77
+ throw new Error("Ollama install completed but binary not found in PATH. Restart your shell and retry.");
78
+ }
79
+ return binary;
80
+ }
81
+ export function startOllamaServe(binary) {
82
+ const child = spawn(binary, ["serve"], {
83
+ detached: true,
84
+ stdio: "ignore",
85
+ windowsHide: true,
86
+ });
87
+ child.unref();
88
+ }
89
+ export async function waitForOllama(baseUrl, timeoutMs = 45000) {
90
+ const deadline = Date.now() + timeoutMs;
91
+ while (Date.now() < deadline) {
92
+ if (await checkOllamaAt(baseUrl))
93
+ return true;
94
+ await sleep(750);
95
+ }
96
+ return false;
97
+ }
98
+ export async function listOllamaModels(baseUrl) {
99
+ try {
100
+ const res = await fetch(`${baseUrl}/api/tags`, { signal: AbortSignal.timeout(8000) });
101
+ if (!res.ok)
102
+ return [];
103
+ const data = (await res.json());
104
+ return (data.models ?? [])
105
+ .map((m) => ({ name: String(m.name ?? "").trim(), size: Number(m.size ?? 0) }))
106
+ .filter((m) => m.name.length > 0);
107
+ }
108
+ catch {
109
+ return [];
110
+ }
111
+ }
112
+ export function findModelMatch(models, preferred) {
113
+ const want = preferred.trim();
114
+ if (!want)
115
+ return null;
116
+ const names = models.map((m) => m.name);
117
+ if (names.includes(want))
118
+ return want;
119
+ const base = want.split(":", 1)[0] ?? want;
120
+ return names.find((n) => n === base || n.startsWith(`${base}:`)) ?? null;
121
+ }
122
+ const FALLBACK_MODEL_CANDIDATES = ["llama3.2:3b", "llama3.1:8b", "phi3:mini"];
123
+ export function pickInstalledFallback(models, preferred) {
124
+ for (const candidate of [preferred, ...FALLBACK_MODEL_CANDIDATES]) {
125
+ const hit = findModelMatch(models, candidate);
126
+ if (hit)
127
+ return hit;
128
+ }
129
+ return models[0]?.name ?? null;
130
+ }
131
+ async function modelIsAvailable(baseUrl, model) {
132
+ const installed = await listOllamaModels(baseUrl);
133
+ return findModelMatch(installed, model) !== null;
134
+ }
135
+ export async function ensureOllamaModel(baseUrl, binary, model) {
136
+ if (await modelIsAvailable(baseUrl, model))
137
+ return;
138
+ workerLog(`Pulling Ollama model ${model}… (one-time download, may take several minutes)`);
139
+ const result = spawnSync(binary, ["pull", model], { stdio: "inherit" });
140
+ if (result.status !== 0) {
141
+ throw new Error(`Failed to pull ${model}. Run manually: ollama pull ${model}`);
142
+ }
143
+ }
144
+ export async function resolveOllamaModel(baseUrl, binary, options = {}) {
145
+ const explicitPreferred = Boolean(options.preferred?.trim());
146
+ const preferred = (options.preferred ?? process.env.GRIDLOCK_OLLAMA_MODEL ?? "llama3.1:8b").trim();
147
+ const interactive = options.interactive ?? isInteractiveTerminal();
148
+ const installed = await listOllamaModels(baseUrl);
149
+ const preferredHit = findModelMatch(installed, preferred);
150
+ if (preferredHit)
151
+ return preferredHit;
152
+ if (explicitPreferred) {
153
+ workerLog(`Pulling requested Ollama model ${preferred}…`);
154
+ await ensureOllamaModel(baseUrl, binary, preferred);
155
+ return preferred;
156
+ }
157
+ if (installed.length === 1) {
158
+ const only = installed[0].name;
159
+ workerLog(`Using installed Ollama model: ${only}`);
160
+ return only;
161
+ }
162
+ if (installed.length > 1) {
163
+ if (interactive) {
164
+ const picked = await promptOllamaModel(installed, preferred);
165
+ if (findModelMatch(installed, picked))
166
+ return picked;
167
+ await ensureOllamaModel(baseUrl, binary, picked);
168
+ return picked;
169
+ }
170
+ const fallback = pickInstalledFallback(installed, preferred);
171
+ if (fallback) {
172
+ workerLog(`Using installed Ollama model: ${fallback}`);
173
+ return fallback;
174
+ }
175
+ }
176
+ workerLog(installed.length === 0
177
+ ? `No Ollama models installed. Pulling ${preferred}…`
178
+ : `Preferred model ${preferred} not installed. Pulling…`);
179
+ await ensureOllamaModel(baseUrl, binary, preferred);
180
+ return preferred;
181
+ }
182
+ export async function bootstrapOllama(baseUrl, modelOptions = {}) {
183
+ const binary = await resolveOllamaBinary();
184
+ if (!(await checkOllamaAt(baseUrl))) {
185
+ workerLog("Ollama not running — starting it…");
186
+ startOllamaServe(binary);
187
+ if (!(await waitForOllama(baseUrl))) {
188
+ throw new Error(`Ollama did not respond at ${baseUrl}.\n` +
189
+ (process.platform === "win32"
190
+ ? " Open the Ollama app from the Start menu (system tray), wait a few seconds, then retry."
191
+ : " Check: systemctl status ollama — or run: ollama serve"));
192
+ }
193
+ }
194
+ return resolveOllamaModel(baseUrl, binary, modelOptions);
195
+ }
@@ -0,0 +1,2 @@
1
+ export declare function parseWorkerWallet(raw: string): `0x${string}`;
2
+ export declare function shortWallet(addr: string): string;
package/dist/wallet.js ADDED
@@ -0,0 +1,14 @@
1
+ import { getAddress, isAddress } from "viem";
2
+ export function parseWorkerWallet(raw) {
3
+ const trimmed = raw.trim();
4
+ if (!trimmed) {
5
+ throw new Error("Wallet address is required.");
6
+ }
7
+ if (!isAddress(trimmed)) {
8
+ throw new Error("Invalid EVM wallet address. Use your EVM wallet 0x address (same as on https://grid-lock.tech/worker).");
9
+ }
10
+ return getAddress(trimmed);
11
+ }
12
+ export function shortWallet(addr) {
13
+ return addr.length >= 10 ? `${addr.slice(0, 6)}…${addr.slice(-4)}` : addr;
14
+ }
@@ -0,0 +1,10 @@
1
+ import type { InferenceBackend } from "./config.js";
2
+ interface WorkerOptions {
3
+ wallet: string;
4
+ backendUrl: string;
5
+ benchmarkOnly?: boolean;
6
+ inference?: InferenceBackend;
7
+ model?: string;
8
+ }
9
+ export declare function startWorker(options: WorkerOptions): Promise<void>;
10
+ export {};