@nodetool-ai/runtime 0.7.0-rc.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bridge-protocol.d.ts +39 -0
- package/dist/bridge-protocol.d.ts.map +1 -0
- package/dist/bridge-protocol.js +39 -0
- package/dist/bridge-protocol.js.map +1 -0
- package/dist/comfy-executor.d.ts +41 -0
- package/dist/comfy-executor.d.ts.map +1 -0
- package/dist/comfy-executor.js +305 -0
- package/dist/comfy-executor.js.map +1 -0
- package/dist/context-packer.d.ts +22 -0
- package/dist/context-packer.d.ts.map +1 -0
- package/dist/context-packer.js +66 -0
- package/dist/context-packer.js.map +1 -0
- package/dist/context.d.ts +405 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +1455 -0
- package/dist/context.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/dist/node-executor.d.ts +53 -0
- package/dist/node-executor.d.ts.map +1 -0
- package/dist/node-executor.js +9 -0
- package/dist/node-executor.js.map +1 -0
- package/dist/providers/aki-manifest.json +34 -0
- package/dist/providers/aki-provider.d.ts +65 -0
- package/dist/providers/aki-provider.d.ts.map +1 -0
- package/dist/providers/aki-provider.js +517 -0
- package/dist/providers/aki-provider.js.map +1 -0
- package/dist/providers/anthropic-provider.d.ts +55 -0
- package/dist/providers/anthropic-provider.d.ts.map +1 -0
- package/dist/providers/anthropic-provider.js +563 -0
- package/dist/providers/anthropic-provider.js.map +1 -0
- package/dist/providers/base-provider.d.ts +194 -0
- package/dist/providers/base-provider.d.ts.map +1 -0
- package/dist/providers/base-provider.js +515 -0
- package/dist/providers/base-provider.js.map +1 -0
- package/dist/providers/cerebras-provider.d.ts +20 -0
- package/dist/providers/cerebras-provider.d.ts.map +1 -0
- package/dist/providers/cerebras-provider.js +52 -0
- package/dist/providers/cerebras-provider.js.map +1 -0
- package/dist/providers/claude-agent-provider.d.ts +107 -0
- package/dist/providers/claude-agent-provider.d.ts.map +1 -0
- package/dist/providers/claude-agent-provider.js +586 -0
- package/dist/providers/claude-agent-provider.js.map +1 -0
- package/dist/providers/cohere-provider.d.ts +36 -0
- package/dist/providers/cohere-provider.d.ts.map +1 -0
- package/dist/providers/cohere-provider.js +116 -0
- package/dist/providers/cohere-provider.js.map +1 -0
- package/dist/providers/cost-calculator.d.ts +109 -0
- package/dist/providers/cost-calculator.d.ts.map +1 -0
- package/dist/providers/cost-calculator.js +475 -0
- package/dist/providers/cost-calculator.js.map +1 -0
- package/dist/providers/deepseek-provider.d.ts +25 -0
- package/dist/providers/deepseek-provider.d.ts.map +1 -0
- package/dist/providers/deepseek-provider.js +58 -0
- package/dist/providers/deepseek-provider.js.map +1 -0
- package/dist/providers/fake-provider.d.ts +52 -0
- package/dist/providers/fake-provider.d.ts.map +1 -0
- package/dist/providers/fake-provider.js +107 -0
- package/dist/providers/fake-provider.js.map +1 -0
- package/dist/providers/fal-provider.d.ts +32 -0
- package/dist/providers/fal-provider.d.ts.map +1 -0
- package/dist/providers/fal-provider.js +445 -0
- package/dist/providers/fal-provider.js.map +1 -0
- package/dist/providers/gemini-provider.d.ts +109 -0
- package/dist/providers/gemini-provider.d.ts.map +1 -0
- package/dist/providers/gemini-provider.js +943 -0
- package/dist/providers/gemini-provider.js.map +1 -0
- package/dist/providers/groq-provider.d.ts +20 -0
- package/dist/providers/groq-provider.d.ts.map +1 -0
- package/dist/providers/groq-provider.js +52 -0
- package/dist/providers/groq-provider.js.map +1 -0
- package/dist/providers/huggingface-provider.d.ts +59 -0
- package/dist/providers/huggingface-provider.d.ts.map +1 -0
- package/dist/providers/huggingface-provider.js +305 -0
- package/dist/providers/huggingface-provider.js.map +1 -0
- package/dist/providers/index.d.ts +69 -0
- package/dist/providers/index.d.ts.map +1 -0
- package/dist/providers/index.js +137 -0
- package/dist/providers/index.js.map +1 -0
- package/dist/providers/jina-provider.d.ts +37 -0
- package/dist/providers/jina-provider.d.ts.map +1 -0
- package/dist/providers/jina-provider.js +118 -0
- package/dist/providers/jina-provider.js.map +1 -0
- package/dist/providers/kie-provider.d.ts +19 -0
- package/dist/providers/kie-provider.d.ts.map +1 -0
- package/dist/providers/kie-provider.js +118 -0
- package/dist/providers/kie-provider.js.map +1 -0
- package/dist/providers/llama-provider.d.ts +49 -0
- package/dist/providers/llama-provider.d.ts.map +1 -0
- package/dist/providers/llama-provider.js +381 -0
- package/dist/providers/llama-provider.js.map +1 -0
- package/dist/providers/lmstudio-provider.d.ts +22 -0
- package/dist/providers/lmstudio-provider.d.ts.map +1 -0
- package/dist/providers/lmstudio-provider.js +63 -0
- package/dist/providers/lmstudio-provider.js.map +1 -0
- package/dist/providers/manifest-models.d.ts +12 -0
- package/dist/providers/manifest-models.d.ts.map +1 -0
- package/dist/providers/manifest-models.js +96 -0
- package/dist/providers/manifest-models.js.map +1 -0
- package/dist/providers/meshy-provider.d.ts +53 -0
- package/dist/providers/meshy-provider.d.ts.map +1 -0
- package/dist/providers/meshy-provider.js +230 -0
- package/dist/providers/meshy-provider.js.map +1 -0
- package/dist/providers/minimax-provider.d.ts +66 -0
- package/dist/providers/minimax-provider.d.ts.map +1 -0
- package/dist/providers/minimax-provider.js +531 -0
- package/dist/providers/minimax-provider.js.map +1 -0
- package/dist/providers/mistral-provider.d.ts +26 -0
- package/dist/providers/mistral-provider.d.ts.map +1 -0
- package/dist/providers/mistral-provider.js +74 -0
- package/dist/providers/mistral-provider.js.map +1 -0
- package/dist/providers/moonshot-provider.d.ts +24 -0
- package/dist/providers/moonshot-provider.d.ts.map +1 -0
- package/dist/providers/moonshot-provider.js +43 -0
- package/dist/providers/moonshot-provider.js.map +1 -0
- package/dist/providers/ollama-provider.d.ts +71 -0
- package/dist/providers/ollama-provider.d.ts.map +1 -0
- package/dist/providers/ollama-provider.js +480 -0
- package/dist/providers/ollama-provider.js.map +1 -0
- package/dist/providers/openai-provider.d.ts +103 -0
- package/dist/providers/openai-provider.d.ts.map +1 -0
- package/dist/providers/openai-provider.js +917 -0
- package/dist/providers/openai-provider.js.map +1 -0
- package/dist/providers/openrouter-provider.d.ts +49 -0
- package/dist/providers/openrouter-provider.d.ts.map +1 -0
- package/dist/providers/openrouter-provider.js +147 -0
- package/dist/providers/openrouter-provider.js.map +1 -0
- package/dist/providers/provider-registry.d.ts +24 -0
- package/dist/providers/provider-registry.d.ts.map +1 -0
- package/dist/providers/provider-registry.js +124 -0
- package/dist/providers/provider-registry.js.map +1 -0
- package/dist/providers/python-provider.d.ts +75 -0
- package/dist/providers/python-provider.d.ts.map +1 -0
- package/dist/providers/python-provider.js +157 -0
- package/dist/providers/python-provider.js.map +1 -0
- package/dist/providers/replicate-provider.d.ts +73 -0
- package/dist/providers/replicate-provider.d.ts.map +1 -0
- package/dist/providers/replicate-provider.js +484 -0
- package/dist/providers/replicate-provider.js.map +1 -0
- package/dist/providers/rodin-provider.d.ts +41 -0
- package/dist/providers/rodin-provider.d.ts.map +1 -0
- package/dist/providers/rodin-provider.js +236 -0
- package/dist/providers/rodin-provider.js.map +1 -0
- package/dist/providers/scripted-provider.d.ts +141 -0
- package/dist/providers/scripted-provider.d.ts.map +1 -0
- package/dist/providers/scripted-provider.js +246 -0
- package/dist/providers/scripted-provider.js.map +1 -0
- package/dist/providers/together-provider.d.ts +20 -0
- package/dist/providers/together-provider.d.ts.map +1 -0
- package/dist/providers/together-provider.js +56 -0
- package/dist/providers/together-provider.js.map +1 -0
- package/dist/providers/types.d.ts +202 -0
- package/dist/providers/types.d.ts.map +1 -0
- package/dist/providers/types.js +2 -0
- package/dist/providers/types.js.map +1 -0
- package/dist/providers/vllm-provider.d.ts +22 -0
- package/dist/providers/vllm-provider.d.ts.map +1 -0
- package/dist/providers/vllm-provider.js +66 -0
- package/dist/providers/vllm-provider.js.map +1 -0
- package/dist/providers/voyage-provider.d.ts +37 -0
- package/dist/providers/voyage-provider.d.ts.map +1 -0
- package/dist/providers/voyage-provider.js +124 -0
- package/dist/providers/voyage-provider.js.map +1 -0
- package/dist/providers/xai-provider.d.ts +24 -0
- package/dist/providers/xai-provider.d.ts.map +1 -0
- package/dist/providers/xai-provider.js +57 -0
- package/dist/providers/xai-provider.js.map +1 -0
- package/dist/python-bridge-types.d.ts +70 -0
- package/dist/python-bridge-types.d.ts.map +1 -0
- package/dist/python-bridge-types.js +2 -0
- package/dist/python-bridge-types.js.map +1 -0
- package/dist/python-node-executor.d.ts +20 -0
- package/dist/python-node-executor.d.ts.map +1 -0
- package/dist/python-node-executor.js +219 -0
- package/dist/python-node-executor.js.map +1 -0
- package/dist/python-stdio-bridge.d.ts +67 -0
- package/dist/python-stdio-bridge.d.ts.map +1 -0
- package/dist/python-stdio-bridge.js +704 -0
- package/dist/python-stdio-bridge.js.map +1 -0
- package/dist/recommended-models.d.ts +17 -0
- package/dist/recommended-models.d.ts.map +1 -0
- package/dist/recommended-models.js +139 -0
- package/dist/recommended-models.js.map +1 -0
- package/dist/telemetry.d.ts +61 -0
- package/dist/telemetry.d.ts.map +1 -0
- package/dist/telemetry.js +128 -0
- package/dist/telemetry.js.map +1 -0
- package/dist/trace-exporters.d.ts +72 -0
- package/dist/trace-exporters.d.ts.map +1 -0
- package/dist/trace-exporters.js +216 -0
- package/dist/trace-exporters.js.map +1 -0
- package/dist/tracing-helpers.d.ts +99 -0
- package/dist/tracing-helpers.d.ts.map +1 -0
- package/dist/tracing-helpers.js +217 -0
- package/dist/tracing-helpers.js.map +1 -0
- package/package.json +64 -0
|
@@ -0,0 +1,704 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Python worker bridge using stdio transport.
|
|
3
|
+
*
|
|
4
|
+
* Communicates over stdin/stdout
|
|
5
|
+
* with length-prefixed msgpack framing instead of WebSocket.
|
|
6
|
+
* This eliminates WebSocket connection instability.
|
|
7
|
+
*
|
|
8
|
+
* Protocol: [4 bytes big-endian length][msgpack payload]
|
|
9
|
+
* Readiness: "NODETOOL_STDIO_READY" on stderr
|
|
10
|
+
* Logs: all Python output goes to stderr
|
|
11
|
+
*/
|
|
12
|
+
import * as msgpack from "@msgpack/msgpack";
|
|
13
|
+
import { randomUUID } from "node:crypto";
|
|
14
|
+
import { spawn } from "node:child_process";
|
|
15
|
+
import { EventEmitter } from "node:events";
|
|
16
|
+
import { existsSync } from "node:fs";
|
|
17
|
+
import { homedir } from "node:os";
|
|
18
|
+
import { basename, join } from "node:path";
|
|
19
|
+
import { createLogger } from "@nodetool-ai/config";
|
|
20
|
+
import { BRIDGE_PROTOCOL_VERSION, MIN_NODETOOL_CORE_VERSION } from "./bridge-protocol.js";
|
|
21
|
+
const log = createLogger("nodetool.runtime.python-stdio-bridge");
|
|
22
|
+
const MAX_BRIDGE_FRAME_SIZE = Number(process.env["NODETOOL_BRIDGE_MAX_FRAME_SIZE"] ?? 256 * 1024 * 1024);
|
|
23
|
+
const PYTHON_BRIDGE_ALLOWED_IN_PRODUCTION = process.env["NODETOOL_ALLOW_PYTHON_BRIDGE_IN_PRODUCTION"] === "1";
|
|
24
|
+
function isProductionMode() {
|
|
25
|
+
return process.env["NODETOOL_ENV"] === "production";
|
|
26
|
+
}
|
|
27
|
+
export class PythonStdioBridge extends EventEmitter {
|
|
28
|
+
_process = null;
|
|
29
|
+
_nodeMetadata = [];
|
|
30
|
+
_loadErrors = [];
|
|
31
|
+
_workerStatus = null;
|
|
32
|
+
_pending = new Map();
|
|
33
|
+
_pendingStream = new Map();
|
|
34
|
+
_options;
|
|
35
|
+
_connected = false;
|
|
36
|
+
_connectPromise = null;
|
|
37
|
+
/** Buffered stdout data waiting to be parsed into frames. */
|
|
38
|
+
_readBuffer = Buffer.alloc(0);
|
|
39
|
+
/** Recent stderr lines from the worker for diagnostics. */
|
|
40
|
+
_recentStderr = [];
|
|
41
|
+
constructor(options = {}) {
|
|
42
|
+
super();
|
|
43
|
+
this._options = options;
|
|
44
|
+
}
|
|
45
|
+
// ── Connection lifecycle ───────────────────────────────────────────
|
|
46
|
+
async connect() {
|
|
47
|
+
if (isProductionMode() && !PYTHON_BRIDGE_ALLOWED_IN_PRODUCTION) {
|
|
48
|
+
throw new Error("Python bridge is disabled in production. Python workers are a local-only feature.");
|
|
49
|
+
}
|
|
50
|
+
await this._spawnAndConnect();
|
|
51
|
+
await this._discover();
|
|
52
|
+
try {
|
|
53
|
+
await this.getWorkerStatus();
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
log.warn("Failed to fetch initial Python worker status; load_errors will be unavailable until next status fetch", err);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
ensureConnected() {
|
|
60
|
+
if (this._connected)
|
|
61
|
+
return Promise.resolve();
|
|
62
|
+
if (!this._connectPromise) {
|
|
63
|
+
this._connectPromise = this.connect().then(() => {
|
|
64
|
+
this._connectPromise = null;
|
|
65
|
+
}, (err) => {
|
|
66
|
+
this._connectPromise = null;
|
|
67
|
+
throw err;
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
return this._connectPromise;
|
|
71
|
+
}
|
|
72
|
+
// ── Spawn & stdio setup ────────────────────────────────────────────
|
|
73
|
+
async _spawnAndConnect() {
|
|
74
|
+
const candidates = this._getPythonLaunchCandidates();
|
|
75
|
+
let lastError = null;
|
|
76
|
+
for (const candidate of candidates) {
|
|
77
|
+
try {
|
|
78
|
+
await this._spawnCandidate(candidate);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
throw (lastError ??
|
|
86
|
+
new Error(candidates.length === 0
|
|
87
|
+
? "No Python interpreter found — Python nodes will not be available"
|
|
88
|
+
: "Failed to start Python worker (stdio)"));
|
|
89
|
+
}
|
|
90
|
+
async _spawnCandidate(candidate) {
|
|
91
|
+
const args = [
|
|
92
|
+
...(candidate.argsPrefix ?? []),
|
|
93
|
+
"-m",
|
|
94
|
+
"nodetool.worker",
|
|
95
|
+
"--stdio",
|
|
96
|
+
...(this._options.workerArgs ?? [])
|
|
97
|
+
];
|
|
98
|
+
return new Promise((resolve, reject) => {
|
|
99
|
+
const proc = spawn(candidate.command, args, {
|
|
100
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
101
|
+
});
|
|
102
|
+
this._process = proc;
|
|
103
|
+
let ready = false;
|
|
104
|
+
let stderrOutput = "";
|
|
105
|
+
let settled = false;
|
|
106
|
+
const startupTimeoutMs = this._options.startupTimeoutMs ?? 20000;
|
|
107
|
+
const startupTimer = setTimeout(() => {
|
|
108
|
+
settleError(new Error(`Python worker did not become ready within ${startupTimeoutMs}ms.` +
|
|
109
|
+
(stderrOutput.trim() ? ` Recent stderr: ${stderrOutput.trim()}` : "")));
|
|
110
|
+
}, startupTimeoutMs);
|
|
111
|
+
const settleError = (error) => {
|
|
112
|
+
if (settled)
|
|
113
|
+
return;
|
|
114
|
+
settled = true;
|
|
115
|
+
clearTimeout(startupTimer);
|
|
116
|
+
reject(error);
|
|
117
|
+
};
|
|
118
|
+
const settleSuccess = () => {
|
|
119
|
+
if (settled)
|
|
120
|
+
return;
|
|
121
|
+
settled = true;
|
|
122
|
+
clearTimeout(startupTimer);
|
|
123
|
+
resolve();
|
|
124
|
+
};
|
|
125
|
+
// Stdout is binary protocol data — accumulate and parse frames.
|
|
126
|
+
proc.stdout.on("data", (chunk) => {
|
|
127
|
+
this._readBuffer = Buffer.concat([this._readBuffer, chunk]);
|
|
128
|
+
this._drainFrames();
|
|
129
|
+
});
|
|
130
|
+
// Stderr carries logs and the readiness signal.
|
|
131
|
+
proc.stderr.on("data", (chunk) => {
|
|
132
|
+
const text = chunk.toString();
|
|
133
|
+
stderrOutput += text;
|
|
134
|
+
this._rememberStderr(text);
|
|
135
|
+
this.emit("stderr", text);
|
|
136
|
+
if (!ready && text.includes("NODETOOL_STDIO_READY")) {
|
|
137
|
+
ready = true;
|
|
138
|
+
this._connected = true;
|
|
139
|
+
settleSuccess();
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
proc.on("error", (err) => {
|
|
143
|
+
if (!ready)
|
|
144
|
+
settleError(err);
|
|
145
|
+
});
|
|
146
|
+
proc.on("exit", (code) => {
|
|
147
|
+
this._connected = false;
|
|
148
|
+
if (!ready) {
|
|
149
|
+
const detail = stderrOutput.trim();
|
|
150
|
+
// The most common cause of this failure in shipped builds is that
|
|
151
|
+
// the JS side was published ahead of the matching nodetool-core
|
|
152
|
+
// Python wheel, so the older worker doesn't know `--stdio`. Give
|
|
153
|
+
// users an actionable message instead of raw argparse output.
|
|
154
|
+
if (/unrecognized arguments:.*--stdio/.test(detail)) {
|
|
155
|
+
settleError(new Error("The bundled Python environment is outdated and does not " +
|
|
156
|
+
"support the new stdio worker protocol. Please reinstall " +
|
|
157
|
+
"the Python environment from Settings → Packages " +
|
|
158
|
+
"(Reinstall environment) to update nodetool-core. " +
|
|
159
|
+
`Worker source: ${candidate.source} (${candidate.command}).`));
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
settleError(new Error(detail ||
|
|
163
|
+
`Python worker (${candidate.source}: ${candidate.command}) exited before startup with code ${code}`));
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
this.emit("exit", code);
|
|
167
|
+
const exitErr = new Error(`Python worker exited with code ${code}`);
|
|
168
|
+
for (const [, req] of this._pending) {
|
|
169
|
+
req.reject(exitErr);
|
|
170
|
+
}
|
|
171
|
+
this._pending.clear();
|
|
172
|
+
for (const [, req] of this._pendingStream) {
|
|
173
|
+
req.reject(exitErr);
|
|
174
|
+
}
|
|
175
|
+
this._pendingStream.clear();
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
// ── Frame reader ───────────────────────────────────────────────────
|
|
180
|
+
/** Extract complete length-prefixed frames from _readBuffer. */
|
|
181
|
+
_drainFrames() {
|
|
182
|
+
while (this._readBuffer.length >= 4) {
|
|
183
|
+
const length = this._readBuffer.readUInt32BE(0);
|
|
184
|
+
if (length > MAX_BRIDGE_FRAME_SIZE) {
|
|
185
|
+
this.emit("error", new Error(`Incoming Python bridge frame exceeds max size (${length} > ${MAX_BRIDGE_FRAME_SIZE})`));
|
|
186
|
+
this.close();
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
if (this._readBuffer.length < 4 + length)
|
|
190
|
+
break; // incomplete frame
|
|
191
|
+
const payload = this._readBuffer.subarray(4, 4 + length);
|
|
192
|
+
this._readBuffer = this._readBuffer.subarray(4 + length);
|
|
193
|
+
try {
|
|
194
|
+
const msg = msgpack.decode(payload);
|
|
195
|
+
this._handleMessage(msg);
|
|
196
|
+
}
|
|
197
|
+
catch (err) {
|
|
198
|
+
this.emit("error", new Error(`Failed to decode msgpack frame: ${err}`));
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
// ── Send ───────────────────────────────────────────────────────────
|
|
203
|
+
_send(msg) {
|
|
204
|
+
if (!this._process?.stdin || !this._connected) {
|
|
205
|
+
throw new Error("Not connected to Python worker");
|
|
206
|
+
}
|
|
207
|
+
const payload = Buffer.from(msgpack.encode(msg));
|
|
208
|
+
if (payload.length > MAX_BRIDGE_FRAME_SIZE) {
|
|
209
|
+
throw new Error(`Outgoing Python bridge frame exceeds max size (${payload.length} > ${MAX_BRIDGE_FRAME_SIZE})`);
|
|
210
|
+
}
|
|
211
|
+
const header = Buffer.alloc(4);
|
|
212
|
+
header.writeUInt32BE(payload.length, 0);
|
|
213
|
+
this._process.stdin.write(header);
|
|
214
|
+
this._process.stdin.write(payload);
|
|
215
|
+
}
|
|
216
|
+
// ── Message dispatch ────────────────────────────────────────────────
|
|
217
|
+
_handleMessage(msg) {
|
|
218
|
+
const type = msg.type;
|
|
219
|
+
const requestId = msg.request_id;
|
|
220
|
+
if (type === "discover" && requestId) {
|
|
221
|
+
const pending = this._pending.get(requestId);
|
|
222
|
+
if (pending) {
|
|
223
|
+
const data = msg.data;
|
|
224
|
+
// Reject the discover promise if the worker's protocol is older
|
|
225
|
+
// than what this build of the JS runtime requires. Workers that
|
|
226
|
+
// pre-date the protocol_version field are treated as version 1
|
|
227
|
+
// (the initial protocol release) — they speak the same wire
|
|
228
|
+
// format as v1, they just don't announce it.
|
|
229
|
+
const workerVersion = typeof data.protocol_version === "number"
|
|
230
|
+
? data.protocol_version
|
|
231
|
+
: 1;
|
|
232
|
+
if (workerVersion < BRIDGE_PROTOCOL_VERSION) {
|
|
233
|
+
this._pending.delete(requestId);
|
|
234
|
+
pending.reject(new Error(`The installed nodetool-core speaks bridge protocol v${workerVersion}, ` +
|
|
235
|
+
`but this Nodetool build requires v${BRIDGE_PROTOCOL_VERSION}. ` +
|
|
236
|
+
`Please reinstall the Python environment from Settings → Packages ` +
|
|
237
|
+
`(Reinstall environment) — it will fetch nodetool-core>=${MIN_NODETOOL_CORE_VERSION}.`));
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
if (workerVersion > BRIDGE_PROTOCOL_VERSION) {
|
|
241
|
+
// Forward-compat: a newer worker is expected to keep speaking
|
|
242
|
+
// older protocols, so we proceed with a warning.
|
|
243
|
+
this.emit("stderr", `[python-bridge] Worker protocol v${workerVersion} is newer than ` +
|
|
244
|
+
`JS runtime v${BRIDGE_PROTOCOL_VERSION}; assuming backward compatibility.\n`);
|
|
245
|
+
}
|
|
246
|
+
this._nodeMetadata = data.nodes;
|
|
247
|
+
this._loadErrors = data.load_errors ?? [];
|
|
248
|
+
pending.resolve({ outputs: {}, blobs: {} });
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
else if (type === "result" && requestId) {
|
|
252
|
+
const streamReq = this._pendingStream.get(requestId);
|
|
253
|
+
if (streamReq) {
|
|
254
|
+
this._pendingStream.delete(requestId);
|
|
255
|
+
streamReq.resolve(msg.data);
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
const pending = this._pending.get(requestId);
|
|
259
|
+
if (pending) {
|
|
260
|
+
this._pending.delete(requestId);
|
|
261
|
+
const data = msg.data;
|
|
262
|
+
pending.resolve({ outputs: data.outputs, blobs: data.blobs ?? {} });
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
else if (type === "error" && requestId) {
|
|
266
|
+
const streamReq = this._pendingStream.get(requestId);
|
|
267
|
+
if (streamReq) {
|
|
268
|
+
this._pendingStream.delete(requestId);
|
|
269
|
+
const data = msg.data;
|
|
270
|
+
const err = new Error(data.error);
|
|
271
|
+
err.traceback = data.traceback;
|
|
272
|
+
streamReq.reject(err);
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
const pending = this._pending.get(requestId);
|
|
276
|
+
if (pending) {
|
|
277
|
+
this._pending.delete(requestId);
|
|
278
|
+
const data = msg.data;
|
|
279
|
+
const err = new Error(data.error);
|
|
280
|
+
err.traceback = data.traceback;
|
|
281
|
+
pending.reject(err);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
else if (type === "chunk" && requestId) {
|
|
285
|
+
const streamReq = this._pendingStream.get(requestId);
|
|
286
|
+
if (streamReq) {
|
|
287
|
+
streamReq.onChunk(msg.data);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
else if (type === "progress" && requestId) {
|
|
291
|
+
const pending = this._pending.get(requestId);
|
|
292
|
+
if (pending?.onProgress) {
|
|
293
|
+
const data = msg.data;
|
|
294
|
+
pending.onProgress({ request_id: requestId, ...data });
|
|
295
|
+
}
|
|
296
|
+
this.emit("progress", msg.data);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
// ── Discover ───────────────────────────────────────────────────────
|
|
300
|
+
async _discover() {
|
|
301
|
+
const requestId = randomUUID();
|
|
302
|
+
return new Promise((resolve, reject) => {
|
|
303
|
+
this._pending.set(requestId, {
|
|
304
|
+
resolve: () => {
|
|
305
|
+
this._pending.delete(requestId);
|
|
306
|
+
resolve();
|
|
307
|
+
},
|
|
308
|
+
reject: (err) => {
|
|
309
|
+
this._pending.delete(requestId);
|
|
310
|
+
reject(err);
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
this._send({ type: "discover", request_id: requestId, data: {} });
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
// ── Node execution ─────────────────────────────────────────────────
|
|
317
|
+
async execute(nodeType, fields, secrets, blobs, onProgress) {
|
|
318
|
+
const requestId = randomUUID();
|
|
319
|
+
return new Promise((resolve, reject) => {
|
|
320
|
+
this._pending.set(requestId, { resolve, reject, onProgress });
|
|
321
|
+
this._send({
|
|
322
|
+
type: "execute",
|
|
323
|
+
request_id: requestId,
|
|
324
|
+
data: { node_type: nodeType, fields, secrets, blobs }
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
async *executeStream(nodeType, fields, secrets, blobs, onProgress) {
|
|
329
|
+
const requestId = randomUUID();
|
|
330
|
+
const chunks = [];
|
|
331
|
+
let done = false;
|
|
332
|
+
let error = null;
|
|
333
|
+
let finalResult = null;
|
|
334
|
+
let emittedCount = 0;
|
|
335
|
+
let resolveWait = null;
|
|
336
|
+
if (onProgress) {
|
|
337
|
+
this._pending.set(requestId, {
|
|
338
|
+
resolve: () => undefined,
|
|
339
|
+
reject: () => undefined,
|
|
340
|
+
onProgress
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
const onChunk = (chunk) => {
|
|
344
|
+
chunks.push({
|
|
345
|
+
outputs: chunk.outputs ?? {},
|
|
346
|
+
blobs: chunk.blobs ?? {}
|
|
347
|
+
});
|
|
348
|
+
if (resolveWait) {
|
|
349
|
+
resolveWait();
|
|
350
|
+
resolveWait = null;
|
|
351
|
+
}
|
|
352
|
+
};
|
|
353
|
+
const streamPromise = new Promise((resolve, reject) => {
|
|
354
|
+
this._pendingStream.set(requestId, { resolve, reject, onChunk });
|
|
355
|
+
});
|
|
356
|
+
streamPromise
|
|
357
|
+
.then((result) => {
|
|
358
|
+
finalResult = {
|
|
359
|
+
outputs: result.outputs ?? {},
|
|
360
|
+
blobs: result.blobs ?? {}
|
|
361
|
+
};
|
|
362
|
+
done = true;
|
|
363
|
+
this._pending.delete(requestId);
|
|
364
|
+
if (resolveWait) {
|
|
365
|
+
resolveWait();
|
|
366
|
+
resolveWait = null;
|
|
367
|
+
}
|
|
368
|
+
})
|
|
369
|
+
.catch((err) => {
|
|
370
|
+
error = err;
|
|
371
|
+
done = true;
|
|
372
|
+
this._pending.delete(requestId);
|
|
373
|
+
if (resolveWait) {
|
|
374
|
+
resolveWait();
|
|
375
|
+
resolveWait = null;
|
|
376
|
+
}
|
|
377
|
+
});
|
|
378
|
+
this._send({
|
|
379
|
+
type: "execute.stream",
|
|
380
|
+
request_id: requestId,
|
|
381
|
+
data: { node_type: nodeType, fields, secrets, blobs }
|
|
382
|
+
});
|
|
383
|
+
while (true) {
|
|
384
|
+
while (chunks.length > 0) {
|
|
385
|
+
emittedCount += 1;
|
|
386
|
+
yield chunks.shift();
|
|
387
|
+
}
|
|
388
|
+
if (done)
|
|
389
|
+
break;
|
|
390
|
+
if (error)
|
|
391
|
+
throw error;
|
|
392
|
+
await new Promise((resolve) => {
|
|
393
|
+
resolveWait = resolve;
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
if (error)
|
|
397
|
+
throw error;
|
|
398
|
+
if (emittedCount === 0 && finalResult) {
|
|
399
|
+
yield finalResult;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
cancel(requestId) {
|
|
403
|
+
this._send({ type: "cancel", request_id: requestId, data: {} });
|
|
404
|
+
}
|
|
405
|
+
getNodeMetadata() {
|
|
406
|
+
return this._nodeMetadata;
|
|
407
|
+
}
|
|
408
|
+
getLoadErrors() {
|
|
409
|
+
return this._loadErrors;
|
|
410
|
+
}
|
|
411
|
+
async getWorkerStatus() {
|
|
412
|
+
const requestId = randomUUID();
|
|
413
|
+
const result = await new Promise((resolve, reject) => {
|
|
414
|
+
this._pendingStream.set(requestId, {
|
|
415
|
+
resolve,
|
|
416
|
+
reject,
|
|
417
|
+
onChunk: () => { }
|
|
418
|
+
});
|
|
419
|
+
this._send({ type: "worker.status", request_id: requestId, data: {} });
|
|
420
|
+
});
|
|
421
|
+
this._workerStatus =
|
|
422
|
+
result;
|
|
423
|
+
this._loadErrors = this._workerStatus.load_errors ?? this._loadErrors;
|
|
424
|
+
return this._workerStatus;
|
|
425
|
+
}
|
|
426
|
+
hasNodeType(nodeType) {
|
|
427
|
+
return this._nodeMetadata.some((n) => n.node_type === nodeType);
|
|
428
|
+
}
|
|
429
|
+
get isConnected() {
|
|
430
|
+
return this._connected;
|
|
431
|
+
}
|
|
432
|
+
// ── Provider bridge methods ────────────────────────────────────────
|
|
433
|
+
async listProviders() {
|
|
434
|
+
const result = await this._providerCall("provider.list", {});
|
|
435
|
+
return result.providers;
|
|
436
|
+
}
|
|
437
|
+
async getProviderModels(providerId, modelType, secrets) {
|
|
438
|
+
const result = await this._providerCall("provider.models", {
|
|
439
|
+
provider: providerId,
|
|
440
|
+
model_type: modelType,
|
|
441
|
+
secrets: secrets ?? {}
|
|
442
|
+
});
|
|
443
|
+
return result.models;
|
|
444
|
+
}
|
|
445
|
+
async providerGenerate(providerId, messages, model, options) {
|
|
446
|
+
const result = await this._providerCall("provider.generate", {
|
|
447
|
+
provider: providerId,
|
|
448
|
+
messages,
|
|
449
|
+
model,
|
|
450
|
+
...options
|
|
451
|
+
});
|
|
452
|
+
return result.message;
|
|
453
|
+
}
|
|
454
|
+
async *providerStream(providerId, messages, model, options) {
|
|
455
|
+
const requestId = randomUUID();
|
|
456
|
+
const chunks = [];
|
|
457
|
+
let done = false;
|
|
458
|
+
let error = null;
|
|
459
|
+
let resolveWait = null;
|
|
460
|
+
const onChunk = (chunk) => {
|
|
461
|
+
chunks.push(chunk);
|
|
462
|
+
if (resolveWait) {
|
|
463
|
+
resolveWait();
|
|
464
|
+
resolveWait = null;
|
|
465
|
+
}
|
|
466
|
+
};
|
|
467
|
+
const streamPromise = new Promise((resolve, reject) => {
|
|
468
|
+
this._pendingStream.set(requestId, { resolve, reject, onChunk });
|
|
469
|
+
});
|
|
470
|
+
streamPromise
|
|
471
|
+
.then(() => {
|
|
472
|
+
done = true;
|
|
473
|
+
if (resolveWait) {
|
|
474
|
+
resolveWait();
|
|
475
|
+
resolveWait = null;
|
|
476
|
+
}
|
|
477
|
+
})
|
|
478
|
+
.catch((err) => {
|
|
479
|
+
error = err;
|
|
480
|
+
done = true;
|
|
481
|
+
if (resolveWait) {
|
|
482
|
+
resolveWait();
|
|
483
|
+
resolveWait = null;
|
|
484
|
+
}
|
|
485
|
+
});
|
|
486
|
+
this._send({
|
|
487
|
+
type: "provider.stream",
|
|
488
|
+
request_id: requestId,
|
|
489
|
+
data: { provider: providerId, messages, model, ...options }
|
|
490
|
+
});
|
|
491
|
+
while (true) {
|
|
492
|
+
while (chunks.length > 0)
|
|
493
|
+
yield chunks.shift();
|
|
494
|
+
if (done)
|
|
495
|
+
break;
|
|
496
|
+
if (error)
|
|
497
|
+
throw error;
|
|
498
|
+
await new Promise((resolve) => {
|
|
499
|
+
resolveWait = resolve;
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
if (error)
|
|
503
|
+
throw error;
|
|
504
|
+
}
|
|
505
|
+
async providerTextToImage(providerId, params, secrets) {
|
|
506
|
+
const result = await this._providerCall("provider.text_to_image", {
|
|
507
|
+
provider: providerId,
|
|
508
|
+
params,
|
|
509
|
+
secrets: secrets ?? {}
|
|
510
|
+
});
|
|
511
|
+
return result.blobs.image;
|
|
512
|
+
}
|
|
513
|
+
async providerImageToImage(providerId, image, params, secrets) {
|
|
514
|
+
const result = await this._providerCall("provider.image_to_image", {
|
|
515
|
+
provider: providerId,
|
|
516
|
+
image,
|
|
517
|
+
params,
|
|
518
|
+
secrets: secrets ?? {}
|
|
519
|
+
});
|
|
520
|
+
return result.blobs.image;
|
|
521
|
+
}
|
|
522
|
+
async *providerTTS(providerId, text, model, options) {
|
|
523
|
+
const requestId = randomUUID();
|
|
524
|
+
const chunks = [];
|
|
525
|
+
let done = false;
|
|
526
|
+
let error = null;
|
|
527
|
+
let resolveWait = null;
|
|
528
|
+
const onChunk = (chunk) => {
|
|
529
|
+
const blobs = chunk.blobs;
|
|
530
|
+
if (blobs?.audio)
|
|
531
|
+
chunks.push(blobs.audio);
|
|
532
|
+
if (resolveWait) {
|
|
533
|
+
resolveWait();
|
|
534
|
+
resolveWait = null;
|
|
535
|
+
}
|
|
536
|
+
};
|
|
537
|
+
const streamPromise = new Promise((resolve, reject) => {
|
|
538
|
+
this._pendingStream.set(requestId, { resolve, reject, onChunk });
|
|
539
|
+
});
|
|
540
|
+
streamPromise
|
|
541
|
+
.then(() => {
|
|
542
|
+
done = true;
|
|
543
|
+
if (resolveWait) {
|
|
544
|
+
resolveWait();
|
|
545
|
+
resolveWait = null;
|
|
546
|
+
}
|
|
547
|
+
})
|
|
548
|
+
.catch((err) => {
|
|
549
|
+
error = err;
|
|
550
|
+
done = true;
|
|
551
|
+
if (resolveWait) {
|
|
552
|
+
resolveWait();
|
|
553
|
+
resolveWait = null;
|
|
554
|
+
}
|
|
555
|
+
});
|
|
556
|
+
this._send({
|
|
557
|
+
type: "provider.tts",
|
|
558
|
+
request_id: requestId,
|
|
559
|
+
data: { provider: providerId, text, model, ...options }
|
|
560
|
+
});
|
|
561
|
+
while (true) {
|
|
562
|
+
while (chunks.length > 0)
|
|
563
|
+
yield chunks.shift();
|
|
564
|
+
if (done)
|
|
565
|
+
break;
|
|
566
|
+
if (error)
|
|
567
|
+
throw error;
|
|
568
|
+
await new Promise((resolve) => {
|
|
569
|
+
resolveWait = resolve;
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
if (error)
|
|
573
|
+
throw error;
|
|
574
|
+
}
|
|
575
|
+
async providerASR(providerId, audio, model, options) {
|
|
576
|
+
const result = await this._providerCall("provider.asr", {
|
|
577
|
+
provider: providerId,
|
|
578
|
+
audio,
|
|
579
|
+
model,
|
|
580
|
+
...options
|
|
581
|
+
});
|
|
582
|
+
const r = result;
|
|
583
|
+
return {
|
|
584
|
+
text: r.text,
|
|
585
|
+
chunks: r.chunks
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
async providerEmbedding(providerId, text, model, dimensions) {
|
|
589
|
+
const result = await this._providerCall("provider.embedding", {
|
|
590
|
+
provider: providerId,
|
|
591
|
+
text,
|
|
592
|
+
model,
|
|
593
|
+
dimensions
|
|
594
|
+
});
|
|
595
|
+
return result.embeddings;
|
|
596
|
+
}
|
|
597
|
+
async _providerCall(type, data) {
|
|
598
|
+
const requestId = randomUUID();
|
|
599
|
+
return new Promise((resolve, reject) => {
|
|
600
|
+
this._pendingStream.set(requestId, {
|
|
601
|
+
resolve,
|
|
602
|
+
reject,
|
|
603
|
+
onChunk: () => { }
|
|
604
|
+
});
|
|
605
|
+
this._send({ type, request_id: requestId, data });
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
// ── Shutdown ───────────────────────────────────────────────────────
|
|
609
|
+
close() {
|
|
610
|
+
if (this._process) {
|
|
611
|
+
// Close stdin — the Python side will exit cleanly.
|
|
612
|
+
this._process.stdin?.end();
|
|
613
|
+
this._process.kill();
|
|
614
|
+
this._process = null;
|
|
615
|
+
}
|
|
616
|
+
this._connected = false;
|
|
617
|
+
}
|
|
618
|
+
/** Check if a Python interpreter can be found (without spawning). */
|
|
619
|
+
hasPython() {
|
|
620
|
+
if (isProductionMode() && !PYTHON_BRIDGE_ALLOWED_IN_PRODUCTION) {
|
|
621
|
+
return false;
|
|
622
|
+
}
|
|
623
|
+
return this._getPythonLaunchCandidates().length > 0;
|
|
624
|
+
}
|
|
625
|
+
getRecentStderrLines(limit = 12) {
|
|
626
|
+
return this._recentStderr.slice(-limit);
|
|
627
|
+
}
|
|
628
|
+
getRecentStderrSummary(limit = 12) {
|
|
629
|
+
const lines = this.getRecentStderrLines(limit)
|
|
630
|
+
.map((line) => line.trim())
|
|
631
|
+
.filter(Boolean)
|
|
632
|
+
.filter((line) => line !== "NODETOOL_STDIO_READY");
|
|
633
|
+
if (lines.length === 0)
|
|
634
|
+
return null;
|
|
635
|
+
return lines.join(" | ");
|
|
636
|
+
}
|
|
637
|
+
_rememberStderr(text) {
|
|
638
|
+
for (const rawLine of text.split(/\r?\n/)) {
|
|
639
|
+
const line = rawLine.trim();
|
|
640
|
+
if (!line)
|
|
641
|
+
continue;
|
|
642
|
+
this._recentStderr.push(line);
|
|
643
|
+
}
|
|
644
|
+
if (this._recentStderr.length > 200) {
|
|
645
|
+
this._recentStderr.splice(0, this._recentStderr.length - 200);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
// ── Python path detection ───────────────────────────────────────────
|
|
649
|
+
_getPythonLaunchCandidates() {
|
|
650
|
+
const explicitPythonPath = this._options.pythonPath ?? process.env.NODETOOL_PYTHON;
|
|
651
|
+
if (explicitPythonPath) {
|
|
652
|
+
return [{ command: explicitPythonPath, source: "NODETOOL_PYTHON" }];
|
|
653
|
+
}
|
|
654
|
+
const condaPrefix = process.env.CONDA_PREFIX;
|
|
655
|
+
if (condaPrefix && this._looksLikeNodeToolEnv(condaPrefix)) {
|
|
656
|
+
const activeEnvPython = process.platform === "win32"
|
|
657
|
+
? join(condaPrefix, "python.exe")
|
|
658
|
+
: join(condaPrefix, "bin", "python");
|
|
659
|
+
if (existsSync(activeEnvPython)) {
|
|
660
|
+
return [{ command: activeEnvPython, source: "active CONDA_PREFIX" }];
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
return this._getManagedPythonPaths().map((command) => ({
|
|
664
|
+
command,
|
|
665
|
+
source: "NodeTool-managed env"
|
|
666
|
+
}));
|
|
667
|
+
}
|
|
668
|
+
_looksLikeNodeToolEnv(envPath) {
|
|
669
|
+
const normalized = envPath.replaceAll("\\", "/").toLowerCase();
|
|
670
|
+
const envName = basename(envPath).toLowerCase();
|
|
671
|
+
return (envName === "nodetool" ||
|
|
672
|
+
envName === "conda_env" ||
|
|
673
|
+
normalized.includes("/nodetool/conda_env"));
|
|
674
|
+
}
|
|
675
|
+
_getManagedPythonPaths() {
|
|
676
|
+
const home = homedir();
|
|
677
|
+
if (process.platform === "win32") {
|
|
678
|
+
return [
|
|
679
|
+
process.env.ALLUSERSPROFILE
|
|
680
|
+
? join(process.env.ALLUSERSPROFILE, "nodetool", "conda_env", "python.exe")
|
|
681
|
+
: join(process.env.APPDATA ?? join(home, "AppData", "Roaming"), "nodetool", "conda_env", "python.exe"),
|
|
682
|
+
join(home, "Miniconda3", "envs", "nodetool", "python.exe"),
|
|
683
|
+
join(home, "miniconda3", "envs", "nodetool", "python.exe"),
|
|
684
|
+
join(home, "Anaconda3", "envs", "nodetool", "python.exe"),
|
|
685
|
+
join(home, "anaconda3", "envs", "nodetool", "python.exe"),
|
|
686
|
+
String.raw `C:\ProgramData\nodetool\conda_env\python.exe`
|
|
687
|
+
].filter((c, i, a) => existsSync(c) && a.indexOf(c) === i);
|
|
688
|
+
}
|
|
689
|
+
if (process.platform === "darwin") {
|
|
690
|
+
return [
|
|
691
|
+
join(home, "nodetool_env", "bin", "python"),
|
|
692
|
+
join(home, "miniconda3", "envs", "nodetool", "bin", "python"),
|
|
693
|
+
join(home, "anaconda3", "envs", "nodetool", "bin", "python")
|
|
694
|
+
].filter((c, i, a) => existsSync(c) && a.indexOf(c) === i);
|
|
695
|
+
}
|
|
696
|
+
return [
|
|
697
|
+
join(home, ".local", "share", "nodetool", "conda_env", "bin", "python"),
|
|
698
|
+
"/opt/nodetool/conda_env/bin/python",
|
|
699
|
+
join(home, "miniconda3", "envs", "nodetool", "bin", "python"),
|
|
700
|
+
join(home, "anaconda3", "envs", "nodetool", "bin", "python")
|
|
701
|
+
].filter((c, i, a) => existsSync(c) && a.indexOf(c) === i);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
//# sourceMappingURL=python-stdio-bridge.js.map
|