@fusionkit/cli 0.1.4 → 0.1.6
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/README.md +26 -4
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +4 -17
- package/dist/commands/ensemble-gateway.js +0 -2
- package/dist/commands/ensemble-records.d.ts +2 -1
- package/dist/commands/ensemble-records.js +3 -1
- package/dist/commands/ensemble.js +3 -4
- package/dist/commands/fusion.js +14 -15
- package/dist/commands/local.js +3 -3
- package/dist/cursor-acp.d.ts +18 -0
- package/dist/cursor-acp.js +206 -0
- package/dist/dashboard.d.ts +65 -0
- package/dist/dashboard.js +587 -0
- package/dist/fusion/env.d.ts +108 -0
- package/dist/fusion/env.js +98 -0
- package/dist/fusion/observability.d.ts +39 -0
- package/dist/fusion/observability.js +227 -0
- package/dist/fusion/preflight.d.ts +12 -0
- package/dist/fusion/preflight.js +42 -0
- package/dist/fusion/stack.d.ts +62 -0
- package/dist/fusion/stack.js +295 -0
- package/dist/fusion-config.d.ts +0 -1
- package/dist/fusion-config.js +0 -6
- package/dist/fusion-init.js +2 -11
- package/dist/fusion-quickstart.d.ts +11 -222
- package/dist/fusion-quickstart.js +57 -759
- package/dist/gateway.d.ts +0 -2
- package/dist/gateway.js +12 -2
- package/dist/local.d.ts +10 -17
- package/dist/local.js +50 -116
- package/dist/shared/options.d.ts +2 -1
- package/dist/shared/options.js +13 -19
- package/dist/shared/proc.d.ts +4 -70
- package/dist/shared/proc.js +3 -228
- package/dist/test/cli.test.js +32 -142
- package/dist/test/dashboard.test.d.ts +1 -0
- package/dist/test/dashboard.test.js +214 -0
- package/dist/test/gateway-e2e.test.js +13 -10
- package/dist/test/local.test.js +4 -4
- package/dist/tools.d.ts +2 -0
- package/dist/tools.js +25 -0
- package/package.json +14 -9
- package/scope/.next/BUILD_ID +1 -1
- package/scope/.next/app-build-manifest.json +12 -12
- package/scope/.next/app-path-routes-manifest.json +3 -3
- package/scope/.next/build-manifest.json +2 -2
- package/scope/.next/prerender-manifest.json +16 -16
- package/scope/.next/server/app/_not-found.html +1 -1
- package/scope/.next/server/app/_not-found.rsc +1 -1
- package/scope/.next/server/app/environments.html +1 -1
- package/scope/.next/server/app/environments.rsc +1 -1
- package/scope/.next/server/app/index.html +1 -1
- package/scope/.next/server/app/index.rsc +1 -1
- package/scope/.next/server/app/models.html +1 -1
- package/scope/.next/server/app/models.rsc +1 -1
- package/scope/.next/server/app-paths-manifest.json +3 -3
- package/scope/.next/server/functions-config-manifest.json +2 -2
- package/scope/.next/server/pages/404.html +1 -1
- package/scope/.next/server/pages/500.html +1 -1
- package/scope/.next/server/server-reference-manifest.json +1 -1
- package/dist/commands/init.d.ts +0 -2
- package/dist/commands/init.js +0 -24
- package/dist/commands/lifecycle.d.ts +0 -2
- package/dist/commands/lifecycle.js +0 -124
- package/dist/commands/plane.d.ts +0 -2
- package/dist/commands/plane.js +0 -38
- package/dist/commands/run.d.ts +0 -2
- package/dist/commands/run.js +0 -149
- package/dist/commands/runner.d.ts +0 -2
- package/dist/commands/runner.js +0 -33
- package/dist/commands/secrets.d.ts +0 -2
- package/dist/commands/secrets.js +0 -21
- /package/scope/.next/static/{5tnFLuvnSbNZNtqRgoot8 → x7wPUCpgS31-5ZHJkcKsU}/_buildManifest.js +0 -0
- /package/scope/.next/static/{5tnFLuvnSbNZNtqRgoot8 → x7wPUCpgS31-5ZHJkcKsU}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The fusion model stack: the single `fusionkit serve` router that fronts every
|
|
3
|
+
* panel model plus synthesis, and the in-process gateway that turns it into the
|
|
4
|
+
* judge-streamed-trajectory front door.
|
|
5
|
+
*/
|
|
6
|
+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { tmpdir } from "node:os";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { MlxBackend, startGateway } from "@fusionkit/model-gateway";
|
|
10
|
+
import { startFusionStepGateway } from "../gateway.js";
|
|
11
|
+
import { createPortlessSession } from "../shared/portless.js";
|
|
12
|
+
import { freePort, spawnLogged, terminate, waitForHttp } from "../shared/proc.js";
|
|
13
|
+
import { defaultKeyEnv, fusionkitPyCommand, loadEnvFileInto } from "./env.js";
|
|
14
|
+
/**
|
|
15
|
+
* Heuristic: does the captured output indicate a permanent failure (bad key,
|
|
16
|
+
* inaccessible model) that a retry cannot fix? Used to fail fast with a clear
|
|
17
|
+
* message instead of burning the retry budget on a hopeless start.
|
|
18
|
+
*/
|
|
19
|
+
function looksPermanentFailure(log) {
|
|
20
|
+
return /401|403|invalid[ _-]?api[ _-]?key|unauthorized|forbidden|authentication|permission|model[^\n]*(not found|does not exist)|no such model|model_not_found/i.test(log);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Default provider base URL when a cloud spec carries no explicit `baseUrl`.
|
|
24
|
+
* fusionkit's `ModelEndpoint` requires `base_url`, so the router config must
|
|
25
|
+
* always set it (the `serve-endpoint` shim filled this in for us before). Mirrors
|
|
26
|
+
* `PROVIDER_DEFAULT_BASE_URL` in fusionkit's openai_endpoint.py.
|
|
27
|
+
*/
|
|
28
|
+
function providerDefaultBaseUrl(provider) {
|
|
29
|
+
switch (provider) {
|
|
30
|
+
case "openai":
|
|
31
|
+
return "https://api.openai.com";
|
|
32
|
+
case "anthropic":
|
|
33
|
+
return "https://api.anthropic.com";
|
|
34
|
+
case "google":
|
|
35
|
+
return "https://generativelanguage.googleapis.com";
|
|
36
|
+
case "openai-compatible":
|
|
37
|
+
return "http://127.0.0.1";
|
|
38
|
+
default: {
|
|
39
|
+
const exhaustive = provider;
|
|
40
|
+
throw new Error(`unknown provider ${String(exhaustive)}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/** Pick the panel spec that backs the judge (by model name), else the first. */
|
|
45
|
+
function judgeSpecFor(specs, judgeModel) {
|
|
46
|
+
const first = specs[0];
|
|
47
|
+
if (first === undefined)
|
|
48
|
+
throw new Error("at least one panel model is required");
|
|
49
|
+
if (judgeModel === undefined)
|
|
50
|
+
return first;
|
|
51
|
+
return specs.find((spec) => spec.model === judgeModel) ?? first;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Build the `fusionkit serve` config (YAML) for the consolidated router: one
|
|
55
|
+
* endpoint per panel model. Cloud models call their provider directly (keyed by
|
|
56
|
+
* `api_key_env`); MLX models are fronted as `openai-compatible` endpoints
|
|
57
|
+
* pointing at their in-process gateway loopback URL. The judge endpoint doubles
|
|
58
|
+
* as the synthesizer. Values are JSON-quoted (valid YAML flow scalars).
|
|
59
|
+
*/
|
|
60
|
+
function routerConfigYaml(input) {
|
|
61
|
+
const lines = ["endpoints:"];
|
|
62
|
+
for (const spec of input.specs) {
|
|
63
|
+
const provider = spec.provider ?? "mlx";
|
|
64
|
+
lines.push(` - id: ${JSON.stringify(spec.id)}`);
|
|
65
|
+
lines.push(` model: ${JSON.stringify(spec.model)}`);
|
|
66
|
+
if (provider === "mlx") {
|
|
67
|
+
lines.push(" provider: openai-compatible");
|
|
68
|
+
lines.push(` base_url: ${JSON.stringify(input.mlxUrls[spec.id] ?? "")}`);
|
|
69
|
+
lines.push(" api_key: not-needed");
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
// `base_url` is required by fusionkit's ModelEndpoint, so always emit one
|
|
73
|
+
// (the spec's, or the provider default).
|
|
74
|
+
const baseUrl = spec.baseUrl ?? providerDefaultBaseUrl(provider);
|
|
75
|
+
lines.push(` provider: ${provider}`);
|
|
76
|
+
lines.push(` base_url: ${JSON.stringify(baseUrl)}`);
|
|
77
|
+
const keyEnv = spec.keyEnv ?? defaultKeyEnv(provider);
|
|
78
|
+
if (keyEnv !== undefined)
|
|
79
|
+
lines.push(` api_key_env: ${JSON.stringify(keyEnv)}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
lines.push(`default_model: ${JSON.stringify(input.judgeId)}`);
|
|
83
|
+
lines.push(`judge_model: ${JSON.stringify(input.judgeId)}`);
|
|
84
|
+
lines.push(`synthesizer_model: ${JSON.stringify(input.judgeId)}`);
|
|
85
|
+
// Generous budget: reasoning models (gpt-5.x) spend tokens on reasoning before
|
|
86
|
+
// producing content, so a small cap can yield an empty answer.
|
|
87
|
+
lines.push("sampling: {temperature: 0.2, top_p: 0.9, max_tokens: 8192}");
|
|
88
|
+
lines.push("");
|
|
89
|
+
return lines.join("\n");
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Spawn the single `fusionkit serve` router fronting every panel model + the
|
|
93
|
+
* synthesizer. MLX specs first get an in-process OpenAI-compatible gateway
|
|
94
|
+
* (loopback) that the router proxies to; cloud specs call their provider
|
|
95
|
+
* directly. Returns the router URL, an id->routerUrl endpoint map, and a close
|
|
96
|
+
* that tears down the router process and any MLX gateways it fronts.
|
|
97
|
+
*/
|
|
98
|
+
export async function startRouter(options) {
|
|
99
|
+
const { specs, report } = options;
|
|
100
|
+
const judgeSpec = judgeSpecFor(specs, options.judgeModel);
|
|
101
|
+
const models = specs.map((spec) => ({ id: spec.id, model: spec.model }));
|
|
102
|
+
const identity = specs.map((spec) => spec.id).sort().join(",");
|
|
103
|
+
const announceStart = (label) => {
|
|
104
|
+
if (report)
|
|
105
|
+
report({ kind: "server.start", id: "router", label });
|
|
106
|
+
else
|
|
107
|
+
options.log(`fusion: starting ${label}...`);
|
|
108
|
+
};
|
|
109
|
+
const announceReady = (detail) => {
|
|
110
|
+
if (report)
|
|
111
|
+
report({ kind: "server.ready", id: "router", detail });
|
|
112
|
+
else
|
|
113
|
+
options.log(`fusion: router ready on ${detail}`);
|
|
114
|
+
};
|
|
115
|
+
announceStart(`router · ${specs.map((spec) => spec.id).join(", ")}`);
|
|
116
|
+
// The router inherits the parent env plus the FusionKit checkout's `.env` (so
|
|
117
|
+
// provider keys load seamlessly), without overriding anything already exported.
|
|
118
|
+
// It calls providers directly and MLX over loopback (never a portless HTTPS
|
|
119
|
+
// URL), so it needs no portless CA — and must keep its default certifi bundle
|
|
120
|
+
// intact to verify real provider certificates.
|
|
121
|
+
const env = { ...process.env };
|
|
122
|
+
if (options.fusionkitDir !== undefined) {
|
|
123
|
+
loadEnvFileInto(join(options.fusionkitDir, ".env"), env);
|
|
124
|
+
}
|
|
125
|
+
const backends = [];
|
|
126
|
+
const gateways = [];
|
|
127
|
+
const mlxUrls = {};
|
|
128
|
+
const closeBackends = async () => {
|
|
129
|
+
await Promise.allSettled(gateways.map((gateway) => gateway.close()));
|
|
130
|
+
await Promise.allSettled(backends.map((backend) => backend.stop()));
|
|
131
|
+
};
|
|
132
|
+
try {
|
|
133
|
+
// MLX backends are memory-heavy (each loads a model into RAM), so they start
|
|
134
|
+
// sequentially before the router that fronts them.
|
|
135
|
+
for (const spec of specs) {
|
|
136
|
+
if ((spec.provider ?? "mlx") !== "mlx")
|
|
137
|
+
continue;
|
|
138
|
+
const backend = new MlxBackend({ model: spec.model });
|
|
139
|
+
await backend.start();
|
|
140
|
+
const gateway = await startGateway({ backend });
|
|
141
|
+
backends.push(backend);
|
|
142
|
+
gateways.push(gateway);
|
|
143
|
+
mlxUrls[spec.id] = gateway.url();
|
|
144
|
+
}
|
|
145
|
+
const config = routerConfigYaml({ specs, mlxUrls, judgeId: judgeSpec.id });
|
|
146
|
+
const configDir = mkdtempSync(join(tmpdir(), "fusion-router-"));
|
|
147
|
+
const configPath = join(configDir, "router.yaml");
|
|
148
|
+
writeFileSync(configPath, config);
|
|
149
|
+
const runner = fusionkitPyCommand(options.fusionkitDir);
|
|
150
|
+
const port = await freePort();
|
|
151
|
+
const proc = spawnLogged(runner.command, [...runner.prefix, "serve", "--config", configPath, "--host", "127.0.0.1", "--port", String(port)], {
|
|
152
|
+
...(runner.cwd !== undefined ? { cwd: runner.cwd } : {}),
|
|
153
|
+
...(options.logsDir !== undefined ? { logFile: join(options.logsDir, "router.log") } : {}),
|
|
154
|
+
env
|
|
155
|
+
});
|
|
156
|
+
proc.child.once("exit", () => rmSync(configDir, { recursive: true, force: true }));
|
|
157
|
+
const url = `http://127.0.0.1:${port}`;
|
|
158
|
+
try {
|
|
159
|
+
await waitForHttp(`${url}/v1/models`, proc, {
|
|
160
|
+
timeoutMs: 60_000,
|
|
161
|
+
label: "fusion router",
|
|
162
|
+
requireOk: true
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
terminate(proc.child);
|
|
167
|
+
// A provider-side rejection (bad key / model) will not be fixed by a
|
|
168
|
+
// retry, so surface the distilled cause with guidance.
|
|
169
|
+
const hint = looksPermanentFailure(proc.log()) ? " (check model names and provider API keys)" : "";
|
|
170
|
+
throw new Error(`${error instanceof Error ? error.message : String(error)}${hint}`);
|
|
171
|
+
}
|
|
172
|
+
announceReady(url);
|
|
173
|
+
const endpoints = Object.fromEntries(specs.map((spec) => [spec.id, url]));
|
|
174
|
+
return {
|
|
175
|
+
url,
|
|
176
|
+
port,
|
|
177
|
+
...(proc.child.pid !== undefined ? { pid: proc.child.pid } : {}),
|
|
178
|
+
endpoints,
|
|
179
|
+
models,
|
|
180
|
+
judgeModel: judgeSpec.id,
|
|
181
|
+
identity,
|
|
182
|
+
close: async () => {
|
|
183
|
+
terminate(proc.child);
|
|
184
|
+
await closeBackends();
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
await closeBackends();
|
|
190
|
+
throw error;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
export async function startFusionStack(options) {
|
|
194
|
+
const report = options.report;
|
|
195
|
+
const portless = options.portless ?? (await createPortlessSession({ enabled: false }));
|
|
196
|
+
// Full override (pre-running per-model endpoints + a pre-running synthesis
|
|
197
|
+
// URL, e.g. tests): use them verbatim and spawn no router.
|
|
198
|
+
const override = options.endpoints !== undefined && options.synthesisUrl !== undefined;
|
|
199
|
+
const judgeModelName = options.judgeModel ?? options.models[0]?.model ?? "";
|
|
200
|
+
const models = options.models.map((spec) => ({ id: spec.id, model: spec.model }));
|
|
201
|
+
let modelEndpoints;
|
|
202
|
+
let fusionBackendUrl;
|
|
203
|
+
let routerClose = () => { };
|
|
204
|
+
if (override) {
|
|
205
|
+
modelEndpoints = options.endpoints;
|
|
206
|
+
fusionBackendUrl = options.synthesisUrl;
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
// Discover-or-spawn the single router (models + synthesis), reusing a
|
|
210
|
+
// compatible running instance (same endpoint id set) across runs.
|
|
211
|
+
const expectedIdentity = options.models.map((spec) => spec.id).sort().join(",");
|
|
212
|
+
const resolved = await portless.discoverOrSpawn({
|
|
213
|
+
name: "router",
|
|
214
|
+
identity: expectedIdentity,
|
|
215
|
+
healthCheck: async (loopbackUrl) => {
|
|
216
|
+
try {
|
|
217
|
+
const response = await fetch(`${loopbackUrl}/v1/models`, { signal: AbortSignal.timeout(2000) });
|
|
218
|
+
if (!response.ok)
|
|
219
|
+
return undefined;
|
|
220
|
+
const body = (await response.json());
|
|
221
|
+
return (body.data ?? [])
|
|
222
|
+
.map((entry) => entry.id)
|
|
223
|
+
.filter((id) => typeof id === "string" && id !== "fusionkit/router")
|
|
224
|
+
.sort()
|
|
225
|
+
.join(",");
|
|
226
|
+
}
|
|
227
|
+
catch {
|
|
228
|
+
return undefined;
|
|
229
|
+
}
|
|
230
|
+
},
|
|
231
|
+
spawn: async () => {
|
|
232
|
+
if (report)
|
|
233
|
+
report({ kind: "server.start", id: "router", label: "router" });
|
|
234
|
+
const router = await startRouter({
|
|
235
|
+
specs: options.models,
|
|
236
|
+
...(options.judgeModel !== undefined ? { judgeModel: options.judgeModel } : {}),
|
|
237
|
+
...(options.fusionkitDir !== undefined ? { fusionkitDir: options.fusionkitDir } : {}),
|
|
238
|
+
...(options.logsDir !== undefined ? { logsDir: options.logsDir } : {}),
|
|
239
|
+
...(report !== undefined ? { report } : {}),
|
|
240
|
+
log: options.log
|
|
241
|
+
});
|
|
242
|
+
return {
|
|
243
|
+
port: router.port,
|
|
244
|
+
...(router.pid !== undefined ? { pid: router.pid } : {}),
|
|
245
|
+
close: router.close
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
// The harness + the in-process step call reach the router over loopback (the
|
|
250
|
+
// portless name is for humans); see the CA-at-startup note in portless.ts.
|
|
251
|
+
modelEndpoints = Object.fromEntries(options.models.map((spec) => [spec.id, resolved.loopbackUrl]));
|
|
252
|
+
fusionBackendUrl = resolved.loopbackUrl;
|
|
253
|
+
routerClose = resolved.close;
|
|
254
|
+
}
|
|
255
|
+
try {
|
|
256
|
+
if (report)
|
|
257
|
+
report({ kind: "gateway.start" });
|
|
258
|
+
// The judge-streamed-trajectory front door: each panel model produces a
|
|
259
|
+
// trajectory and the judge emits the trajectory the user's tool executes.
|
|
260
|
+
const gatewayConfig = {
|
|
261
|
+
fusionBackendUrl,
|
|
262
|
+
repo: options.repo,
|
|
263
|
+
outputRoot: options.outputRoot,
|
|
264
|
+
harnesses: ["agent"],
|
|
265
|
+
models,
|
|
266
|
+
judgeModel: judgeModelName,
|
|
267
|
+
modelEndpoints,
|
|
268
|
+
...(options.timeoutMs !== undefined ? { timeoutMs: options.timeoutMs } : {})
|
|
269
|
+
};
|
|
270
|
+
const gateway = await startFusionStepGateway({
|
|
271
|
+
config: gatewayConfig,
|
|
272
|
+
host: options.host ?? "127.0.0.1",
|
|
273
|
+
port: options.port ?? 0,
|
|
274
|
+
...(options.authToken !== undefined ? { authToken: options.authToken } : {})
|
|
275
|
+
});
|
|
276
|
+
// The gateway runs in-process (dies with this CLI), so it gets a per-run
|
|
277
|
+
// portless name rather than being a cross-run singleton.
|
|
278
|
+
const fusionUrl = portless.register("gateway", gateway.port());
|
|
279
|
+
if (report)
|
|
280
|
+
report({ kind: "gateway.ready", detail: fusionUrl });
|
|
281
|
+
return {
|
|
282
|
+
fusionUrl,
|
|
283
|
+
endpoints: modelEndpoints,
|
|
284
|
+
close: async () => {
|
|
285
|
+
await gateway.close();
|
|
286
|
+
portless.unregister("gateway");
|
|
287
|
+
await routerClose();
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
catch (error) {
|
|
292
|
+
await routerClose();
|
|
293
|
+
throw error;
|
|
294
|
+
}
|
|
295
|
+
}
|
package/dist/fusion-config.d.ts
CHANGED
package/dist/fusion-config.js
CHANGED
|
@@ -96,12 +96,6 @@ export function parseFusionConfig(raw, source) {
|
|
|
96
96
|
throw new FusionConfigError(`${source}: portless must be a boolean`);
|
|
97
97
|
config.portless = raw.portless;
|
|
98
98
|
}
|
|
99
|
-
if (raw.cursorKitDir !== undefined && raw.cursorKitDir !== null) {
|
|
100
|
-
if (typeof raw.cursorKitDir !== "string") {
|
|
101
|
-
throw new FusionConfigError(`${source}: cursorKitDir must be a string or null`);
|
|
102
|
-
}
|
|
103
|
-
config.cursorKitDir = raw.cursorKitDir;
|
|
104
|
-
}
|
|
105
99
|
if (raw.port !== undefined && raw.port !== null) {
|
|
106
100
|
if (typeof raw.port !== "number" || !Number.isInteger(raw.port) || raw.port < 0) {
|
|
107
101
|
throw new FusionConfigError(`${source}: port must be a non-negative integer or null`);
|
package/dist/fusion-init.js
CHANGED
|
@@ -51,7 +51,7 @@ export async function runFusionInit(input) {
|
|
|
51
51
|
options: [
|
|
52
52
|
{ value: "codex", label: "codex", hint: "OpenAI Codex CLI" },
|
|
53
53
|
{ value: "claude", label: "claude", hint: "Claude Code" },
|
|
54
|
-
{ value: "cursor", label: "cursor", hint: "cursor-agent (
|
|
54
|
+
{ value: "cursor", label: "cursor", hint: "cursor-agent (logged-in CLI)" },
|
|
55
55
|
{ value: "serve", label: "serve", hint: "just run the gateway and print setup" }
|
|
56
56
|
],
|
|
57
57
|
defaultIndex: 0
|
|
@@ -83,14 +83,6 @@ export async function runFusionInit(input) {
|
|
|
83
83
|
}
|
|
84
84
|
const judgeDefault = panel[0]?.model ?? "";
|
|
85
85
|
const judgeModel = await text({ message: "Judge model (for synthesis)", defaultValue: judgeDefault });
|
|
86
|
-
let cursorKitDir;
|
|
87
|
-
if (tool === "cursor") {
|
|
88
|
-
const answer = await text({
|
|
89
|
-
message: "Cursorkit checkout dir (optional, can set FUSIONKIT_CURSORKIT_DIR later)"
|
|
90
|
-
});
|
|
91
|
-
if (answer.length > 0)
|
|
92
|
-
cursorKitDir = answer;
|
|
93
|
-
}
|
|
94
86
|
const observe = await confirm({ message: "Enable the observability dashboard by default?", defaultValue: false });
|
|
95
87
|
const config = {
|
|
96
88
|
version: FUSION_CONFIG_VERSION,
|
|
@@ -98,8 +90,7 @@ export async function runFusionInit(input) {
|
|
|
98
90
|
panel,
|
|
99
91
|
...(judgeModel.length > 0 ? { judgeModel } : {}),
|
|
100
92
|
local: preset === "local",
|
|
101
|
-
observe
|
|
102
|
-
...(cursorKitDir !== undefined ? { cursorKitDir } : {})
|
|
93
|
+
observe
|
|
103
94
|
};
|
|
104
95
|
let path;
|
|
105
96
|
try {
|
|
@@ -10,229 +10,18 @@
|
|
|
10
10
|
*
|
|
11
11
|
* No mocks: the panel is real models, candidates are real patches verified by
|
|
12
12
|
* really running the repo's tests, and the judge is a real model.
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
13
|
+
*
|
|
14
|
+
* This module is the run orchestrator; the supporting pieces live in `./fusion/`
|
|
15
|
+
* (env + defaults, the observability dashboard, the model stack, and preflight)
|
|
16
|
+
* and are re-exported here so existing import paths keep working.
|
|
17
|
+
*/
|
|
18
|
+
import type { FusionTool, RunFusionOptions } from "./fusion/env.js";
|
|
19
|
+
export * from "./fusion/env.js";
|
|
20
|
+
export * from "./fusion/observability.js";
|
|
21
|
+
export * from "./fusion/stack.js";
|
|
22
|
+
export * from "./fusion/preflight.js";
|
|
23
|
+
/** Launchable fusion tools (registry-derived) plus the `serve` pseudo-tool. */
|
|
18
24
|
export declare const FUSION_TOOLS: readonly FusionTool[];
|
|
19
|
-
export type PanelProvider = "mlx" | "openai" | "anthropic" | "google" | "openai-compatible";
|
|
20
|
-
/**
|
|
21
|
-
* One panel model. `mlx` models run locally via the in-repo provisioner; cloud
|
|
22
|
-
* providers (openai/anthropic/google/openai-compatible) are fronted as
|
|
23
|
-
* OpenAI-compatible endpoints by FusionKit's `serve-endpoint` command, run via
|
|
24
|
-
* `uvx fusionkit` (no checkout required).
|
|
25
|
-
*/
|
|
26
|
-
export type PanelModelSpec = {
|
|
27
|
-
id: string;
|
|
28
|
-
model: string;
|
|
29
|
-
provider?: PanelProvider;
|
|
30
|
-
baseUrl?: string;
|
|
31
|
-
keyEnv?: string;
|
|
32
|
-
};
|
|
33
|
-
/**
|
|
34
|
-
* The PyPI version of the `fusionkit` Python distribution that provides the
|
|
35
|
-
* synthesizer (`fusionkit serve`) and the single-model OpenAI shim
|
|
36
|
-
* (`fusionkit serve-endpoint`). Pinned so `uvx` resolves a reproducible build.
|
|
37
|
-
*/
|
|
38
|
-
export declare const FUSIONKIT_PYPI_VERSION = "0.2.0";
|
|
39
|
-
/**
|
|
40
|
-
* Default cloud panel — works cross-platform with only `OPENAI_API_KEY` and
|
|
41
|
-
* `ANTHROPIC_API_KEY` set. The judge defaults to the first entry.
|
|
42
|
-
*/
|
|
43
|
-
export declare const DEFAULT_CLOUD_PANEL: readonly PanelModelSpec[];
|
|
44
|
-
/** The locally cached MLX trio (Apple Silicon only) used behind `--local`. */
|
|
45
|
-
export declare const DEFAULT_TRIO: readonly PanelModelSpec[];
|
|
46
|
-
/**
|
|
47
|
-
* How to invoke the `fusionkit` Python CLI: from PyPI via `uvx` by default
|
|
48
|
-
* (no checkout), or from a local checkout via `uv run` when `fusionkitDir` is
|
|
49
|
-
* given (a dev override). Returns the command plus the argv prefix that
|
|
50
|
-
* precedes the subcommand (`serve`, `serve-endpoint`, ...).
|
|
51
|
-
*/
|
|
52
|
-
export declare function fusionkitPyCommand(fusionkitDir?: string): {
|
|
53
|
-
command: string;
|
|
54
|
-
prefix: string[];
|
|
55
|
-
cwd?: string;
|
|
56
|
-
};
|
|
57
|
-
/**
|
|
58
|
-
* Parse a `.env` file (KEY=VALUE lines, `#` comments, optional `export`,
|
|
59
|
-
* single/double quotes) and fill any keys not already present in `env`.
|
|
60
|
-
* Existing env values win, so an explicitly exported key is never overridden.
|
|
61
|
-
*/
|
|
62
|
-
export declare function loadEnvFileInto(path: string, env: Record<string, string | undefined>): void;
|
|
63
|
-
/** Default env var holding the API key for each cloud provider. */
|
|
64
|
-
export declare function defaultKeyEnv(provider: PanelProvider): string | undefined;
|
|
65
|
-
/** The git repository root containing `dir`, or undefined if it is not in a repo. */
|
|
66
|
-
export declare function gitToplevel(dir: string): string | undefined;
|
|
67
|
-
/**
|
|
68
|
-
* Compute the binaries and API keys the run requires given the tool, panel, and
|
|
69
|
-
* options. Pre-running endpoints (`--model-endpoint`) and a pre-running
|
|
70
|
-
* `--synthesis-url` drop the corresponding requirements.
|
|
71
|
-
*/
|
|
72
|
-
export declare function preflightRequirements(tool: FusionTool, models: PanelModelSpec[], options: RunFusionOptions): {
|
|
73
|
-
requiredBins: string[];
|
|
74
|
-
requiredEnv: string[];
|
|
75
|
-
};
|
|
76
|
-
/** Fixed port for the local observability dashboard (the scope app). */
|
|
77
|
-
export declare const SCOPE_DASHBOARD_PORT = 4317;
|
|
78
|
-
/**
|
|
79
|
-
* Locate the isolated scope dashboard app (handoffkit/apps/scope) by walking up
|
|
80
|
-
* from this module. Works from both the compiled dist and src layouts. Only the
|
|
81
|
-
* monorepo dev fallback uses this — published installs ship a prebuilt bundle.
|
|
82
|
-
*/
|
|
83
|
-
export declare function findScopeAppDir(): string;
|
|
84
|
-
/**
|
|
85
|
-
* Path to the prebuilt, self-contained dashboard server staged into the CLI
|
|
86
|
-
* package (`scope/server.js`, a sibling of `dist/`), or undefined when it is
|
|
87
|
-
* absent — i.e. a monorepo dev checkout where the bundle was never staged. Both
|
|
88
|
-
* the compiled `dist/fusion-quickstart.js` and the `src/` layout resolve to the
|
|
89
|
-
* same `<cli-package>/scope/server.js`.
|
|
90
|
-
*/
|
|
91
|
-
export declare function bundledScopeServer(): string | undefined;
|
|
92
|
-
export type Observability = {
|
|
93
|
-
url: string;
|
|
94
|
-
ingestUrl: string;
|
|
95
|
-
traceDir: string;
|
|
96
|
-
close: () => Promise<void>;
|
|
97
|
-
};
|
|
98
|
-
export declare function startObservability(input: {
|
|
99
|
-
log: (line: string) => void;
|
|
100
|
-
logFile?: string;
|
|
101
|
-
report?: StackReporter;
|
|
102
|
-
portless: PortlessSession;
|
|
103
|
-
}): Promise<Observability>;
|
|
104
|
-
/**
|
|
105
|
-
* The single `fusionkit serve` router: one process that fronts every panel
|
|
106
|
-
* model (passthrough, routed by the endpoint id in the request `model` field)
|
|
107
|
-
* and also performs trajectory synthesis. `endpoints` maps each panel id to the
|
|
108
|
-
* router URL so the harness reaches its model through the one base URL.
|
|
109
|
-
*/
|
|
110
|
-
export type Router = {
|
|
111
|
-
url: string;
|
|
112
|
-
port: number;
|
|
113
|
-
/** The router process pid (owns its portless route across runs). */
|
|
114
|
-
pid?: number;
|
|
115
|
-
endpoints: Record<string, string>;
|
|
116
|
-
models: EnsembleModel[];
|
|
117
|
-
/** The endpoint id used as the judge/synthesizer. */
|
|
118
|
-
judgeModel: string;
|
|
119
|
-
/** Sorted endpoint ids — the router's discover-or-spawn identity token. */
|
|
120
|
-
identity: string;
|
|
121
|
-
close: () => Promise<void>;
|
|
122
|
-
};
|
|
123
|
-
/**
|
|
124
|
-
* Structured boot progress. When a reporter is supplied the stack emits these
|
|
125
|
-
* events instead of the plain `fusion: ...` log lines, so a live TUI (or any
|
|
126
|
-
* other consumer) can render per-stage status. Without one, callers keep getting
|
|
127
|
-
* the existing line logs.
|
|
128
|
-
*/
|
|
129
|
-
export type StackEvent = {
|
|
130
|
-
kind: "server.start";
|
|
131
|
-
id: string;
|
|
132
|
-
label: string;
|
|
133
|
-
} | {
|
|
134
|
-
kind: "server.ready";
|
|
135
|
-
id: string;
|
|
136
|
-
detail: string;
|
|
137
|
-
} | {
|
|
138
|
-
kind: "server.fail";
|
|
139
|
-
id: string;
|
|
140
|
-
detail: string;
|
|
141
|
-
} | {
|
|
142
|
-
kind: "synth.start";
|
|
143
|
-
} | {
|
|
144
|
-
kind: "synth.ready";
|
|
145
|
-
detail: string;
|
|
146
|
-
} | {
|
|
147
|
-
kind: "gateway.start";
|
|
148
|
-
} | {
|
|
149
|
-
kind: "gateway.ready";
|
|
150
|
-
detail: string;
|
|
151
|
-
} | {
|
|
152
|
-
kind: "dashboard.start";
|
|
153
|
-
} | {
|
|
154
|
-
kind: "dashboard.ready";
|
|
155
|
-
detail: string;
|
|
156
|
-
} | {
|
|
157
|
-
kind: "dashboard.fail";
|
|
158
|
-
detail: string;
|
|
159
|
-
};
|
|
160
|
-
export type StackReporter = (event: StackEvent) => void;
|
|
161
|
-
/**
|
|
162
|
-
* Spawn the single `fusionkit serve` router fronting every panel model + the
|
|
163
|
-
* synthesizer. MLX specs first get an in-process OpenAI-compatible gateway
|
|
164
|
-
* (loopback) that the router proxies to; cloud specs call their provider
|
|
165
|
-
* directly. Returns the router URL, an id->routerUrl endpoint map, and a close
|
|
166
|
-
* that tears down the router process and any MLX gateways it fronts.
|
|
167
|
-
*/
|
|
168
|
-
export declare function startRouter(options: {
|
|
169
|
-
specs: PanelModelSpec[];
|
|
170
|
-
judgeModel?: string;
|
|
171
|
-
fusionkitDir?: string;
|
|
172
|
-
logsDir?: string;
|
|
173
|
-
report?: StackReporter;
|
|
174
|
-
log: (line: string) => void;
|
|
175
|
-
}): Promise<Router>;
|
|
176
|
-
export type FusionStack = {
|
|
177
|
-
fusionUrl: string;
|
|
178
|
-
endpoints: Record<string, string>;
|
|
179
|
-
close: () => Promise<void>;
|
|
180
|
-
};
|
|
181
|
-
export type StartFusionStackOptions = {
|
|
182
|
-
repo: string;
|
|
183
|
-
outputRoot: string;
|
|
184
|
-
models: PanelModelSpec[];
|
|
185
|
-
endpoints?: Record<string, string>;
|
|
186
|
-
fusionkitDir?: string;
|
|
187
|
-
judgeModel?: string;
|
|
188
|
-
/** Pre-running fusionkit serve URL for trajectory synthesis (skips spawn). */
|
|
189
|
-
synthesisUrl?: string;
|
|
190
|
-
host?: string;
|
|
191
|
-
port?: number;
|
|
192
|
-
authToken?: string;
|
|
193
|
-
timeoutMs?: number;
|
|
194
|
-
logsDir?: string;
|
|
195
|
-
report?: StackReporter;
|
|
196
|
-
/** Active portless session; defaults to a disabled (loopback) session. */
|
|
197
|
-
portless?: PortlessSession;
|
|
198
|
-
log: (line: string) => void;
|
|
199
|
-
};
|
|
200
|
-
export declare function startFusionStack(options: StartFusionStackOptions): Promise<FusionStack>;
|
|
201
|
-
/**
|
|
202
|
-
* Start the Cursorkit bridge with its local-model backend pointed at the fusion
|
|
203
|
-
* gateway, and resolve once it is listening. Returns the child and its port.
|
|
204
|
-
*/
|
|
205
|
-
export declare function startCursorBridge(input: {
|
|
206
|
-
cursorKitDir: string;
|
|
207
|
-
fusionUrl: string;
|
|
208
|
-
logFile?: string;
|
|
209
|
-
caCertPath?: string;
|
|
210
|
-
log: (line: string) => void;
|
|
211
|
-
}): Promise<{
|
|
212
|
-
child: ChildProcess;
|
|
213
|
-
port: number;
|
|
214
|
-
}>;
|
|
215
|
-
export type RunFusionOptions = {
|
|
216
|
-
models?: PanelModelSpec[];
|
|
217
|
-
endpoints?: Record<string, string>;
|
|
218
|
-
fusionkitDir?: string;
|
|
219
|
-
repo?: string;
|
|
220
|
-
judgeModel?: string;
|
|
221
|
-
synthesisUrl?: string;
|
|
222
|
-
cursorKitDir?: string;
|
|
223
|
-
authToken?: string;
|
|
224
|
-
port?: number;
|
|
225
|
-
timeoutMs?: number;
|
|
226
|
-
/** Use the local MLX panel trio (Apple Silicon) instead of the cloud panel. */
|
|
227
|
-
local?: boolean;
|
|
228
|
-
/** Boot the local scope dashboard and stream trace events into it. */
|
|
229
|
-
observe?: boolean;
|
|
230
|
-
/** Skip the interactive cost/scope confirmation for the cloud panel. */
|
|
231
|
-
yes?: boolean;
|
|
232
|
-
/** Route services through portless (stable named URLs + singletons). Default on. */
|
|
233
|
-
portless?: boolean;
|
|
234
|
-
log?: (line: string) => void;
|
|
235
|
-
};
|
|
236
25
|
/** Whether portless is enabled: explicit flag/config wins, else on unless PORTLESS=0. */
|
|
237
26
|
export declare function portlessEnabled(options: RunFusionOptions): boolean;
|
|
238
27
|
export declare function runFusion(tool: FusionTool, toolArgs: string[], options?: RunFusionOptions): Promise<number>;
|