mlx-bun 0.0.13 → 0.2.0
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 +31 -21
- package/bin/mlx-bun.mjs +1 -1
- package/docs/{design → archive}/hlg-sampling.md +3 -3
- package/docs/{investigations → archive/investigations}/hlg-sampling-investigation.md +1 -1
- package/docs/archive/investigations/pareto-specialized-runtime-findings.md +92 -0
- package/docs/{planning → archive/planning}/chat-ui-vision.md +1 -1
- package/docs/{planning → archive/planning}/release-notes-v0.0.9.md +1 -1
- package/docs/{planning → archive/planning}/website-readme-pass-plan.md +1 -1
- package/docs/design/colibri-glm52-port.md +1 -1
- package/docs/design/compat-cli-surface-design.md +1 -1
- package/docs/design/decode-speed-program.md +1 -1
- package/docs/design/docs-reorg-plan.md +1 -1
- package/docs/design/dspark-serving-program.md +1 -1
- package/docs/design/dspark-speculative-decoding.md +53 -3
- package/docs/design/grammar-spec-batching-integration.md +1 -1
- package/docs/design/mlx-lm-serving-execution-seam.md +81 -0
- package/docs/design/orpo-dynamic-lambda.md +1 -1
- package/docs/design/orpo-future-enhancements.md +1 -1
- package/docs/design/orpo-training.md +2 -2
- package/docs/design/parity-tier-dag.md +1 -1
- package/docs/design/repo-cleanup-plan.md +46 -4
- package/docs/design/segmented-backward-training.md +2 -2
- package/docs/design/ssd-kv-cold-tier.md +127 -0
- package/docs/design/turboquant-weights.md +467 -0
- package/docs/design/w4a16-compute-precision-spike.md +1 -1
- package/docs/design/web-chat-redesign.md +5 -5
- package/docs/planning/chunk-task-roadmap.md +1 -1
- package/docs/planning/curve-sampler-research-plan.md +4 -4
- package/docs/planning/memory-docs-and-dag-plan.md +1 -1
- package/docs/planning/release-notes-v0.2.0.md +49 -0
- package/docs/planning/video-series.md +421 -0
- package/docs/reference/cli.md +15 -4
- package/docs/reference/distribution.md +1 -0
- package/docs/reference/features-matrix.md +3 -2
- package/docs/reference/library-api.md +41 -1
- package/docs/reference/models.md +33 -0
- package/docs/reference/server-api.md +69 -14
- package/docs/reference/server-config.md +57 -33
- package/package.json +2 -2
- package/src/anthropic.ts +92 -3
- package/src/cli.ts +131 -36
- package/src/eval/kl.ts +27 -20
- package/src/eval/runner.ts +23 -4
- package/src/eval/tasks/bfcl.ts +2 -1
- package/src/eval/tasks/gsm8k.ts +2 -1
- package/src/eval/tasks/hashhop.ts +2 -1
- package/src/eval/tasks/humaneval.ts +5 -4
- package/src/eval/tasks/ifeval.ts +2 -1
- package/src/eval/tasks/mmlu.ts +2 -1
- package/src/expert-io.ts +3 -1
- package/src/expert-offload.ts +3 -1
- package/src/expert-trace.ts +3 -1
- package/src/fit.ts +24 -43
- package/src/flags.ts +7 -9
- package/src/generate.ts +107 -92
- package/src/grammar.ts +2 -1
- package/src/harness-pi.ts +1 -1
- package/src/index.ts +20 -0
- package/src/jobs/job-entry.ts +3 -2
- package/src/kv-scheme.ts +199 -0
- package/src/kv-store.ts +370 -205
- package/src/media-fetch.ts +19 -3
- package/src/memory/model.ts +2 -1
- package/src/memory/vault.ts +2 -1
- package/src/memory-plan.ts +18 -0
- package/src/mlx/ffi.ts +8 -11
- package/src/mlx/ops.ts +31 -6
- package/src/model/batched-mask.ts +1 -1
- package/src/model/batched-quant.ts +1 -1
- package/src/model/batched-rotating-quant.ts +43 -97
- package/src/model/batched-rotating-state.ts +73 -0
- package/src/model/batched-rotating.ts +56 -105
- package/src/model/batched-row-storage.ts +127 -0
- package/src/model/compiled-decode.ts +3 -2
- package/src/model/factory.ts +44 -49
- package/src/model/gemma4-base.ts +163 -10
- package/src/model/generated/gemma4-12b.ts +50 -146
- package/src/model/generated/gemma4-26b.ts +32 -92
- package/src/model/generated/gemma4-e4b.ts +26 -74
- package/src/model/glm52-cache.ts +19 -9
- package/src/model/glm52-memory.ts +28 -1
- package/src/model/minicpm5.ts +2 -1
- package/src/model/profile.ts +433 -0
- package/src/model/qwen3-delta.ts +98 -2
- package/src/model/qwen3-mrope.ts +252 -0
- package/src/model/qwen3_5.ts +217 -25
- package/src/native/frame_extract.swift +140 -0
- package/src/native-pack.ts +13 -6
- package/src/pi-web.ts +6 -5
- package/src/prompt-cache.ts +2 -2
- package/src/quantize/atomic-output.ts +38 -0
- package/src/quantize/calibration.ts +2 -1
- package/src/quantize/config-writer.ts +6 -0
- package/src/quantize/index.ts +28 -1
- package/src/quantize/job.ts +11 -0
- package/src/quantize/quantizer.ts +265 -100
- package/src/quantize/rotate.ts +603 -0
- package/src/quantize/safetensors-writer.ts +79 -1
- package/src/quantize/sensitivity.ts +41 -59
- package/src/quantize/weight-transform.ts +292 -0
- package/src/responses.ts +102 -2
- package/src/runtime-config.ts +67 -0
- package/src/sampler.ts +272 -0
- package/src/serve/admin-routes.ts +184 -0
- package/src/serve/aux-routes.ts +132 -0
- package/src/serve/batch-scheduler.ts +200 -87
- package/src/serve/completion-executor.ts +346 -0
- package/src/serve/completion-sink.ts +157 -0
- package/src/serve/discovery-routes.ts +205 -0
- package/src/serve/generation-gateway.ts +116 -109
- package/src/serve/kv-budget.ts +14 -0
- package/src/serve/lab-routes.ts +338 -0
- package/src/serve/model-admin-routes.ts +153 -0
- package/src/serve/prompt-response-trace.ts +147 -0
- package/src/serve/request-plan.ts +158 -0
- package/src/serve/static-routes.ts +101 -0
- package/src/server.ts +704 -1400
- package/src/spec/dflash-source.ts +2 -1
- package/src/spec/dspark/module-dflash.ts +1 -1
- package/src/spec/glm52-mtp-source.ts +8 -0
- package/src/spec/qwen-mtp-source.ts +431 -0
- package/src/spec/serve-loop.ts +36 -68
- package/src/ssd-cache.ts +17 -14
- package/src/ssd-durability.ts +195 -0
- package/src/train/flash-cce.ts +12 -11
- package/src/train/forward.ts +32 -1
- package/src/train/loss.ts +2 -1
- package/src/train/segmented.ts +4 -3
- package/src/train/trainer.ts +3 -2
- package/src/vision/preprocess.ts +1 -1
- package/src/vision/prompt.ts +47 -1
- package/src/vision/qwen3vl-preprocess.ts +409 -0
- package/src/vision/qwen3vl-prompt.ts +196 -0
- package/src/vision/qwen3vl-tower.ts +513 -0
- package/src/vision/video-frames.ts +148 -0
- package/src/web/app.js +10 -3
- package/src/web/src/chat.ts +12 -5
- package/src/weights.ts +81 -26
- /package/docs/{investigations → archive/investigations}/chunk-finetune-experiment.md +0 -0
- /package/docs/{investigations → archive/investigations}/colibri-oracle-pin.md +0 -0
- /package/docs/{investigations → archive/investigations}/curve-bisector-routing.md +0 -0
- /package/docs/{investigations → archive/investigations}/curve-runs/bisector-route.json +0 -0
- /package/docs/{investigations → archive/investigations}/curve-runs/bisector-trace.json +0 -0
- /package/docs/{investigations → archive/investigations}/curve-runs/curve-terrain.json +0 -0
- /package/docs/{investigations → archive/investigations}/curve-runs/curve-terrain.svg +0 -0
- /package/docs/{investigations → archive/investigations}/curve-terrain.html +0 -0
- /package/docs/{investigations → archive/investigations}/decode-roofline-lookagain.md +0 -0
- /package/docs/{investigations → archive/investigations}/dspark-handoff.md +0 -0
- /package/docs/{investigations → archive/investigations}/expert-offload-single-user-moe.md +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-ab.html +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-explorer.html +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-figs/cliff.svg +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-figs/cross.svg +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-figs/edges.svg +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-figs/k20-grids.svg +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-figs/k20-peaks.svg +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-figs/lw-curve.svg +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-figs/terrain.svg +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-report.html +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-ab.json +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-blind-prompts.md +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-blind.json +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-cliff.json +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-cross.json +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-edge.json +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-eval1.json +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-frontier.json +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-grid.json +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-grid2.json +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-lw-coarse.json +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-map.json +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/run0-original.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runA-recommended.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runAA-cliff-cpm5.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runAB-grid2-e4b.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runAC-peak-e4b-partial.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runAD-ab-samples.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runAE-edge-e4b.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runAF-logit-calibration.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runAG-eval-config2.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runB-gain-variants.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runC-pivots.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runD-oetf.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runE-eotf.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runF-shaper-working.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runG-shaper-sweep.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runH-wsweep.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runI-asweep.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runJ-knobmap.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runK-frontier.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runL-widemap.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runM-frontier-emb.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runN-verify.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runO-frontier-seed5000.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runP-grid-AxSm.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runQ-grid-tgW.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runR-blind-transcript.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runS-cross-coherence.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runT-cross-cpm5.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runU-cross-12b.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runV-lw-e4b.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runW-lw-12b.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runX-lw-cpm5.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runY-cliff-e4b.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runZ-cliff-12b.log +0 -0
- /package/docs/{investigations → archive/investigations}/indexshare-performance-spike.md +0 -0
- /package/docs/{investigations → archive/investigations}/kernel-perf-review-2026-07.md +0 -0
- /package/docs/{investigations → archive/investigations}/lab-build-journal.md +0 -0
- /package/docs/{investigations → archive/investigations}/openwiki-evaluation.md +0 -0
- /package/docs/{investigations → archive/investigations}/orpo-base-uf-experiment-and-directions.md +0 -0
- /package/docs/{investigations → archive/investigations}/orpo-flash-cce-pin-leak.md +0 -0
- /package/docs/{investigations → archive/investigations}/orpo-uf-testing-handoff.md +0 -0
- /package/docs/{investigations → archive/investigations}/pi-builtin-investigation.md +0 -0
- /package/docs/{investigations → archive/investigations}/segmented-backward-handoff.md +0 -0
- /package/docs/{investigations → archive/investigations}/starter-model-port-handoff.md +0 -0
- /package/docs/{investigations → archive/investigations}/steel-flash-cce-handoff.md +0 -0
- /package/docs/{investigations → archive/investigations}/trainer-validation-experiment.md +0 -0
- /package/docs/{planning → archive/planning}/chat-ui-vision-memory-first.md +0 -0
- /package/docs/{planning → archive/planning}/journal.md +0 -0
- /package/docs/{planning → archive/planning}/release-notes-v0.0.10.md +0 -0
- /package/docs/{planning → archive/planning}/release-notes-v0.0.11.md +0 -0
- /package/docs/{planning → archive/planning}/release-notes-v0.0.12.md +0 -0
- /package/docs/{planning → archive/planning}/release-notes-v0.0.13.md +0 -0
- /package/docs/{planning → archive/planning}/web-ui-pass-plan.md +0 -0
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
import type { JobStore } from "../jobs";
|
|
2
|
+
|
|
3
|
+
export interface LabRouteDeps {
|
|
4
|
+
ensureJobs(): Promise<JobStore>;
|
|
5
|
+
serverPort(): number | undefined;
|
|
6
|
+
invalidateLibrary(): void;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type LabRoute =
|
|
10
|
+
| "dataset-templates"
|
|
11
|
+
| "dataset-submit"
|
|
12
|
+
| "quantize-inspect"
|
|
13
|
+
| "model-resolve-folder"
|
|
14
|
+
| "quantize-submit"
|
|
15
|
+
| "finetune-inspect-dataset"
|
|
16
|
+
| "finetune-submit"
|
|
17
|
+
| "finetune-merge"
|
|
18
|
+
| "finetune-export";
|
|
19
|
+
|
|
20
|
+
export function matchLabRoute(method: string, pathname: string): LabRoute | null {
|
|
21
|
+
switch (`${method} ${pathname}`) {
|
|
22
|
+
case "GET /api/dataset/templates": return "dataset-templates";
|
|
23
|
+
case "POST /api/dataset/submit": return "dataset-submit";
|
|
24
|
+
case "POST /api/quantize/inspect": return "quantize-inspect";
|
|
25
|
+
case "POST /api/quantize/resolve-folder":
|
|
26
|
+
case "POST /api/model/resolve-folder":
|
|
27
|
+
return "model-resolve-folder";
|
|
28
|
+
case "POST /api/quantize/submit": return "quantize-submit";
|
|
29
|
+
case "POST /api/finetune/inspect-dataset": return "finetune-inspect-dataset";
|
|
30
|
+
case "POST /api/finetune/submit": return "finetune-submit";
|
|
31
|
+
case "POST /api/finetune/merge": return "finetune-merge";
|
|
32
|
+
case "POST /api/finetune/export": return "finetune-export";
|
|
33
|
+
default: return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function handleLabRoute(
|
|
38
|
+
url: URL,
|
|
39
|
+
request: Request,
|
|
40
|
+
deps: LabRouteDeps,
|
|
41
|
+
): Promise<Response | null> {
|
|
42
|
+
switch (matchLabRoute(request.method, url.pathname)) {
|
|
43
|
+
case "dataset-templates": {
|
|
44
|
+
const { TEMPLATES } = await import("../dataset");
|
|
45
|
+
return Response.json({ templates: TEMPLATES });
|
|
46
|
+
}
|
|
47
|
+
case "dataset-submit": {
|
|
48
|
+
const body = (await request.json().catch(() => ({}))) as {
|
|
49
|
+
template_id?: string;
|
|
50
|
+
inputs?: Record<string, unknown>;
|
|
51
|
+
model_name?: string;
|
|
52
|
+
};
|
|
53
|
+
const { getTemplate } = await import("../dataset");
|
|
54
|
+
if (!body.template_id || !getTemplate(body.template_id)) {
|
|
55
|
+
return Response.json(
|
|
56
|
+
{ ok: false, error: `unknown template ${body.template_id}` },
|
|
57
|
+
{ status: 400 },
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
const store = await deps.ensureJobs();
|
|
61
|
+
const { submitInProcess } = await import("../jobs");
|
|
62
|
+
const { homedir } = await import("node:os");
|
|
63
|
+
const safe = body.template_id.replace(/[^a-z0-9_-]/gi, "");
|
|
64
|
+
const outDir = `${homedir()}/.cache/mlx-bun/datasets/dataset-${safe}-${Date.now()}`;
|
|
65
|
+
const { jobId } = submitInProcess(store, "dataset", {
|
|
66
|
+
template_id: body.template_id,
|
|
67
|
+
inputs: body.inputs ?? {},
|
|
68
|
+
output_dir: outDir,
|
|
69
|
+
api_url: `http://127.0.0.1:${deps.serverPort()}`,
|
|
70
|
+
model_name: body.model_name ?? "local",
|
|
71
|
+
}, outDir);
|
|
72
|
+
return Response.json({ ok: true, job_id: jobId, output_dir: outDir });
|
|
73
|
+
}
|
|
74
|
+
case "quantize-inspect": {
|
|
75
|
+
const body = (await request.json().catch(() => ({}))) as { model_id?: string };
|
|
76
|
+
const { inspectModel } = await import("../quantize");
|
|
77
|
+
return Response.json(await inspectModel(body.model_id ?? ""));
|
|
78
|
+
}
|
|
79
|
+
case "model-resolve-folder":
|
|
80
|
+
return resolveModelFolder(request);
|
|
81
|
+
case "quantize-submit":
|
|
82
|
+
return submitQuantize(request, deps);
|
|
83
|
+
case "finetune-inspect-dataset": {
|
|
84
|
+
const body = (await request.json().catch(() => ({}))) as { path?: string };
|
|
85
|
+
const { inspectDataset } = await import("../train");
|
|
86
|
+
return Response.json(await inspectDataset(body.path ?? ""));
|
|
87
|
+
}
|
|
88
|
+
case "finetune-submit": {
|
|
89
|
+
const body = (await request.json().catch(() => ({}))) as Record<string, unknown>;
|
|
90
|
+
if (!body.model_dir || !body.data_dir) {
|
|
91
|
+
return Response.json(
|
|
92
|
+
{ ok: false, error: "model_dir and data_dir required" },
|
|
93
|
+
{ status: 400 },
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
const store = await deps.ensureJobs();
|
|
97
|
+
const { submitSubprocess } = await import("../jobs");
|
|
98
|
+
const { homedir } = await import("node:os");
|
|
99
|
+
const adapterPath = (body.adapter_path as string) ||
|
|
100
|
+
`${homedir()}/.cache/mlx-bun/adapters/adapter-${Date.now()}`;
|
|
101
|
+
const { jobId } = submitSubprocess(
|
|
102
|
+
store,
|
|
103
|
+
"finetune",
|
|
104
|
+
{ ...body, adapter_path: adapterPath },
|
|
105
|
+
adapterPath,
|
|
106
|
+
);
|
|
107
|
+
return Response.json({ ok: true, job_id: jobId, adapter_path: adapterPath });
|
|
108
|
+
}
|
|
109
|
+
case "finetune-merge": {
|
|
110
|
+
const body = (await request.json().catch(() => ({}))) as {
|
|
111
|
+
adapter_a?: string;
|
|
112
|
+
adapter_b?: string;
|
|
113
|
+
scales?: number[];
|
|
114
|
+
};
|
|
115
|
+
if (!body.adapter_a || !body.adapter_b) {
|
|
116
|
+
return Response.json(
|
|
117
|
+
{ ok: false, error: "adapter_a and adapter_b required" },
|
|
118
|
+
{ status: 400 },
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
try {
|
|
122
|
+
const { mergeAdapters } = await import("../train");
|
|
123
|
+
const { homedir } = await import("node:os");
|
|
124
|
+
const mergedPath = `${homedir()}/.cache/mlx-bun/adapters/merged-${Date.now()}`;
|
|
125
|
+
const stats = await mergeAdapters(
|
|
126
|
+
[body.adapter_a, body.adapter_b],
|
|
127
|
+
mergedPath,
|
|
128
|
+
body.scales,
|
|
129
|
+
);
|
|
130
|
+
return Response.json({ ok: true, merged_path: mergedPath, stats });
|
|
131
|
+
} catch (error) {
|
|
132
|
+
return Response.json({ ok: false, error: (error as Error).message }, { status: 400 });
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
case "finetune-export": {
|
|
136
|
+
const body = (await request.json().catch(() => ({}))) as {
|
|
137
|
+
base_model?: string;
|
|
138
|
+
adapter_path?: string;
|
|
139
|
+
method?: string;
|
|
140
|
+
};
|
|
141
|
+
if (!body.base_model || !body.adapter_path) {
|
|
142
|
+
return Response.json(
|
|
143
|
+
{ ok: false, error: "base_model and adapter_path required" },
|
|
144
|
+
{ status: 400 },
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
try {
|
|
148
|
+
const { exportAdapter } = await import("../train");
|
|
149
|
+
const { homedir } = await import("node:os");
|
|
150
|
+
const exportPath = `${homedir()}/.cache/mlx-bun/exports/export-${Date.now()}`;
|
|
151
|
+
const manifest = await exportAdapter(
|
|
152
|
+
exportPath,
|
|
153
|
+
body.base_model,
|
|
154
|
+
body.adapter_path,
|
|
155
|
+
body.method,
|
|
156
|
+
);
|
|
157
|
+
return Response.json({ ok: true, export_path: exportPath, manifest });
|
|
158
|
+
} catch (error) {
|
|
159
|
+
return Response.json({ ok: false, error: (error as Error).message }, { status: 400 });
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
default:
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async function resolveModelFolder(request: Request): Promise<Response> {
|
|
168
|
+
const body = (await request.json().catch(() => ({}))) as {
|
|
169
|
+
folder_name?: string;
|
|
170
|
+
rel_path?: string;
|
|
171
|
+
};
|
|
172
|
+
const { statSync, readdirSync, readFileSync } = await import("node:fs");
|
|
173
|
+
const { join } = await import("node:path");
|
|
174
|
+
const { homedir } = await import("node:os");
|
|
175
|
+
const hubRoot = process.env.HF_HUB_CACHE ??
|
|
176
|
+
(process.env.HF_HOME
|
|
177
|
+
? join(process.env.HF_HOME, "hub")
|
|
178
|
+
: join(homedir(), ".cache/huggingface/hub"));
|
|
179
|
+
const roots = [hubRoot, join(homedir(), ".cache/mlx-bun")];
|
|
180
|
+
const hasConfig = (dir: string) => {
|
|
181
|
+
try {
|
|
182
|
+
return statSync(join(dir, "config.json")).isFile();
|
|
183
|
+
} catch {
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
const folder = (body.folder_name ?? "")
|
|
188
|
+
.replace(/\\/g, "/")
|
|
189
|
+
.split("/")
|
|
190
|
+
.filter(Boolean)
|
|
191
|
+
.pop() ?? "";
|
|
192
|
+
const relSegments = (body.rel_path ?? "")
|
|
193
|
+
.replace(/\\/g, "/")
|
|
194
|
+
.split("/")
|
|
195
|
+
.filter(Boolean);
|
|
196
|
+
const configDir = relSegments.length >= 2 ? relSegments[relSegments.length - 2] : "";
|
|
197
|
+
const repoIdOf = (modelsDir: string) =>
|
|
198
|
+
modelsDir.slice("models--".length).replaceAll("--", "/");
|
|
199
|
+
const pickSnapshot = (repoDir: string): string | null => {
|
|
200
|
+
const snapshots = join(repoDir, "snapshots");
|
|
201
|
+
let head = "";
|
|
202
|
+
try {
|
|
203
|
+
head = readFileSync(join(repoDir, "refs", "main"), "utf8").trim();
|
|
204
|
+
} catch {}
|
|
205
|
+
if (head && hasConfig(join(snapshots, head))) return join(snapshots, head);
|
|
206
|
+
try {
|
|
207
|
+
for (const hash of readdirSync(snapshots)) {
|
|
208
|
+
if (hasConfig(join(snapshots, hash))) return join(snapshots, hash);
|
|
209
|
+
}
|
|
210
|
+
} catch {}
|
|
211
|
+
return null;
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
if (folder.startsWith("models--")) {
|
|
215
|
+
const path = pickSnapshot(join(hubRoot, folder));
|
|
216
|
+
if (path) return Response.json({ ok: true, path, repo_id: repoIdOf(folder) });
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const hashDir = configDir || folder;
|
|
220
|
+
if (hashDir) {
|
|
221
|
+
try {
|
|
222
|
+
for (const repo of readdirSync(hubRoot)) {
|
|
223
|
+
if (!repo.startsWith("models--")) continue;
|
|
224
|
+
const candidate = join(hubRoot, repo, "snapshots", hashDir);
|
|
225
|
+
if (hasConfig(candidate)) {
|
|
226
|
+
return Response.json({ ok: true, path: candidate, repo_id: repoIdOf(repo) });
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
} catch {}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
for (const root of roots) {
|
|
233
|
+
if (folder && hasConfig(join(root, folder))) {
|
|
234
|
+
return Response.json({ ok: true, path: join(root, folder) });
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const { Registry } = await import("../registry");
|
|
239
|
+
const registry = new Registry();
|
|
240
|
+
try {
|
|
241
|
+
await registry.scan();
|
|
242
|
+
const all = registry.list();
|
|
243
|
+
const record = (folder.startsWith("models--")
|
|
244
|
+
? all.find((model) => model.repoId === repoIdOf(folder))
|
|
245
|
+
: undefined) ??
|
|
246
|
+
all.find((model) => model.path.split("/").pop() === hashDir) ??
|
|
247
|
+
all.find((model) => model.repoId.split("/").pop() === folder);
|
|
248
|
+
if (record) {
|
|
249
|
+
return Response.json({
|
|
250
|
+
ok: true,
|
|
251
|
+
path: record.path,
|
|
252
|
+
repo_id: record.repoId,
|
|
253
|
+
model_type: record.modelType,
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
} finally {
|
|
257
|
+
registry.close();
|
|
258
|
+
}
|
|
259
|
+
return Response.json({
|
|
260
|
+
ok: false,
|
|
261
|
+
error: "Couldn't locate this folder on disk — paste the path instead.",
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
async function submitQuantize(request: Request, deps: LabRouteDeps): Promise<Response> {
|
|
266
|
+
const body = (await request.json().catch(() => ({}))) as {
|
|
267
|
+
model_id?: string;
|
|
268
|
+
bits?: number;
|
|
269
|
+
group_size?: number;
|
|
270
|
+
target_bpw?: number;
|
|
271
|
+
candidate_bits?: number[];
|
|
272
|
+
reference?: string;
|
|
273
|
+
calibration_mix?: string;
|
|
274
|
+
n_calibration?: number;
|
|
275
|
+
rotate_weights?: boolean;
|
|
276
|
+
rotation_seed?: number;
|
|
277
|
+
};
|
|
278
|
+
if (!body.model_id)
|
|
279
|
+
return Response.json({ ok: false, error: "model_id required" }, { status: 400 });
|
|
280
|
+
const store = await deps.ensureJobs();
|
|
281
|
+
const { submitSubprocess } = await import("../jobs");
|
|
282
|
+
const { homedir } = await import("node:os");
|
|
283
|
+
const { join } = await import("node:path");
|
|
284
|
+
const { mkdirSync, writeFileSync } = await import("node:fs");
|
|
285
|
+
const { createHash } = await import("node:crypto");
|
|
286
|
+
const bits = body.bits ?? 4;
|
|
287
|
+
const groupSize = body.group_size ?? 64;
|
|
288
|
+
const snapshotMatch = body.model_id.match(/(models--[^/]+)\/snapshots\//);
|
|
289
|
+
let org = "local";
|
|
290
|
+
let name: string;
|
|
291
|
+
if (snapshotMatch) {
|
|
292
|
+
const parts = snapshotMatch[1]!.split("--");
|
|
293
|
+
org = parts[1] ?? "local";
|
|
294
|
+
name = parts.slice(2).join("--");
|
|
295
|
+
} else if (
|
|
296
|
+
body.model_id.includes("/") &&
|
|
297
|
+
!body.model_id.startsWith("/") &&
|
|
298
|
+
!body.model_id.startsWith("~")
|
|
299
|
+
) {
|
|
300
|
+
const segments = body.model_id.split("/");
|
|
301
|
+
org = segments[0]!;
|
|
302
|
+
name = segments.slice(1).join("-");
|
|
303
|
+
} else {
|
|
304
|
+
name = body.model_id.split("/").filter(Boolean).at(-1) ?? "model";
|
|
305
|
+
}
|
|
306
|
+
name = (name || "model").replace(/[^a-z0-9_.-]/gi, "");
|
|
307
|
+
org = (org || "local").replace(/[^a-z0-9_.-]/gi, "");
|
|
308
|
+
const suffix = `${body.target_bpw ? `mixed-${body.target_bpw}bpw` : `${bits}bit`}` +
|
|
309
|
+
`${body.rotate_weights ? `-rot${body.rotation_seed ?? 42}` : ""}`;
|
|
310
|
+
const quantRepo = `${name}-OptiQ-${suffix}`;
|
|
311
|
+
const hubRoot = process.env.HF_HUB_CACHE ??
|
|
312
|
+
(process.env.HF_HOME
|
|
313
|
+
? join(process.env.HF_HOME, "hub")
|
|
314
|
+
: join(homedir(), ".cache/huggingface/hub"));
|
|
315
|
+
const repoDir = join(hubRoot, `models--${org}--${quantRepo}`);
|
|
316
|
+
const snapshotHash = createHash("sha1").update(`${org}/${quantRepo}`).digest("hex");
|
|
317
|
+
const outDir = join(repoDir, "snapshots", snapshotHash);
|
|
318
|
+
try {
|
|
319
|
+
mkdirSync(join(repoDir, "refs"), { recursive: true });
|
|
320
|
+
writeFileSync(join(repoDir, "refs", "main"), snapshotHash);
|
|
321
|
+
} catch {}
|
|
322
|
+
const { jobId } = submitSubprocess(store, "quantize", {
|
|
323
|
+
model_id: body.model_id,
|
|
324
|
+
out_dir: outDir,
|
|
325
|
+
bits,
|
|
326
|
+
group_size: groupSize,
|
|
327
|
+
target_bpw: body.target_bpw,
|
|
328
|
+
candidate_bits: body.candidate_bits,
|
|
329
|
+
reference: body.reference,
|
|
330
|
+
calibration_mix: body.calibration_mix,
|
|
331
|
+
n_calibration: body.n_calibration,
|
|
332
|
+
rotate_weights: body.rotate_weights,
|
|
333
|
+
rotation_seed: body.rotation_seed,
|
|
334
|
+
}, outDir, {
|
|
335
|
+
onComplete: () => deps.invalidateLibrary(),
|
|
336
|
+
});
|
|
337
|
+
return Response.json({ ok: true, job_id: jobId, output_dir: outDir });
|
|
338
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import type { ServerContext } from "../server";
|
|
2
|
+
import type { GenerationGateway } from "./generation-gateway";
|
|
3
|
+
import { embedMany, isEmbeddingModel } from "../embed";
|
|
4
|
+
import { listAvailableAdapters } from "../lora";
|
|
5
|
+
|
|
6
|
+
export type ModelAdminRoute =
|
|
7
|
+
| { kind: "embeddings" }
|
|
8
|
+
| { kind: "adapters-available" }
|
|
9
|
+
| { kind: "adapters-list" }
|
|
10
|
+
| { kind: "adapters-mount" }
|
|
11
|
+
| { kind: "adapters-unmount"; id: string };
|
|
12
|
+
|
|
13
|
+
export function matchModelAdminRoute(method: string, pathname: string): ModelAdminRoute | null {
|
|
14
|
+
switch (`${method} ${pathname}`) {
|
|
15
|
+
case "POST /v1/embeddings": return { kind: "embeddings" };
|
|
16
|
+
case "GET /v1/adapters/available": return { kind: "adapters-available" };
|
|
17
|
+
case "GET /v1/adapters": return { kind: "adapters-list" };
|
|
18
|
+
case "POST /v1/adapters": return { kind: "adapters-mount" };
|
|
19
|
+
}
|
|
20
|
+
if (method === "DELETE" && pathname.startsWith("/v1/adapters/")) {
|
|
21
|
+
return {
|
|
22
|
+
kind: "adapters-unmount",
|
|
23
|
+
id: decodeURIComponent(pathname.slice("/v1/adapters/".length)),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function handleModelAdminRoute(
|
|
30
|
+
url: URL,
|
|
31
|
+
request: Request,
|
|
32
|
+
ctx: ServerContext,
|
|
33
|
+
gateway: Pick<GenerationGateway, "runExclusive">,
|
|
34
|
+
): Promise<Response | null> {
|
|
35
|
+
const route = matchModelAdminRoute(request.method, url.pathname);
|
|
36
|
+
if (!route) return null;
|
|
37
|
+
|
|
38
|
+
switch (route.kind) {
|
|
39
|
+
case "embeddings": {
|
|
40
|
+
if (!isEmbeddingModel(ctx.model)) {
|
|
41
|
+
return Response.json({
|
|
42
|
+
error: {
|
|
43
|
+
message: `served model "${ctx.modelId}" is not an embedding model; ` +
|
|
44
|
+
"serve an embedding model (e.g. Qwen3-Embedding) to use /v1/embeddings",
|
|
45
|
+
type: "invalid_request_error",
|
|
46
|
+
},
|
|
47
|
+
}, { status: 400 });
|
|
48
|
+
}
|
|
49
|
+
let body: { input?: string | string[]; instruction?: string };
|
|
50
|
+
try {
|
|
51
|
+
body = (await request.json()) as typeof body;
|
|
52
|
+
} catch {
|
|
53
|
+
return Response.json({ error: { message: "invalid JSON body" } }, { status: 400 });
|
|
54
|
+
}
|
|
55
|
+
const inputs = Array.isArray(body.input)
|
|
56
|
+
? body.input
|
|
57
|
+
: body.input != null
|
|
58
|
+
? [body.input]
|
|
59
|
+
: [];
|
|
60
|
+
if (inputs.length === 0 || !inputs.every((input) => typeof input === "string")) {
|
|
61
|
+
return Response.json({
|
|
62
|
+
error: {
|
|
63
|
+
message: "`input` must be a string or array of strings",
|
|
64
|
+
type: "invalid_request_error",
|
|
65
|
+
},
|
|
66
|
+
}, { status: 400 });
|
|
67
|
+
}
|
|
68
|
+
const instruction = typeof body.instruction === "string" ? body.instruction : undefined;
|
|
69
|
+
const results = embedMany(ctx.model, ctx.tokenizer, inputs, instruction);
|
|
70
|
+
let totalTokens = 0;
|
|
71
|
+
const data = results.map((result, index) => {
|
|
72
|
+
totalTokens += result.tokens;
|
|
73
|
+
return { object: "embedding", index, embedding: Array.from(result.vector) };
|
|
74
|
+
});
|
|
75
|
+
return Response.json({
|
|
76
|
+
object: "list",
|
|
77
|
+
data,
|
|
78
|
+
model: ctx.modelId,
|
|
79
|
+
usage: { prompt_tokens: totalTokens, total_tokens: totalTokens },
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
case "adapters-available": {
|
|
84
|
+
const { homedir } = await import("node:os");
|
|
85
|
+
const stores = [
|
|
86
|
+
`${homedir()}/.cache/mlx-bun-finetunes`,
|
|
87
|
+
`${homedir()}/.cache/mlx-bun/adapters`,
|
|
88
|
+
];
|
|
89
|
+
const mounted = new Set(ctx.adapters.list().map((adapter) => adapter.id));
|
|
90
|
+
const bareName = (value: string) => value.split("/").pop()!.toLowerCase();
|
|
91
|
+
const servedName = bareName(ctx.modelId);
|
|
92
|
+
const adapters = (await listAvailableAdapters(stores)).map((adapter) => ({
|
|
93
|
+
id: adapter.id,
|
|
94
|
+
path: adapter.path,
|
|
95
|
+
rank: adapter.rank,
|
|
96
|
+
scale: adapter.scale,
|
|
97
|
+
base_model: adapter.baseModel,
|
|
98
|
+
mounted: mounted.has(adapter.id),
|
|
99
|
+
compatible: adapter.baseModel == null || bareName(adapter.baseModel) === servedName,
|
|
100
|
+
}));
|
|
101
|
+
return Response.json({ adapters });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
case "adapters-list":
|
|
105
|
+
return Response.json({
|
|
106
|
+
adapters: ctx.adapters.list().map((adapter) => ({
|
|
107
|
+
id: adapter.id,
|
|
108
|
+
path: adapter.path,
|
|
109
|
+
rank: adapter.rank,
|
|
110
|
+
scale: adapter.scale,
|
|
111
|
+
size_bytes: adapter.sizeBytes,
|
|
112
|
+
mounted_layers: adapter.mountedLayers,
|
|
113
|
+
ram_bytes: adapter.ramBytes,
|
|
114
|
+
})),
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
case "adapters-mount": {
|
|
118
|
+
let body: { id?: string; path?: string };
|
|
119
|
+
try {
|
|
120
|
+
body = (await request.json()) as typeof body;
|
|
121
|
+
} catch {
|
|
122
|
+
return Response.json({ error: { message: "invalid JSON body" } }, { status: 400 });
|
|
123
|
+
}
|
|
124
|
+
if (!body.id || !body.path) {
|
|
125
|
+
return Response.json({ error: { message: "id and path required" } }, { status: 400 });
|
|
126
|
+
}
|
|
127
|
+
try {
|
|
128
|
+
const info = await gateway.runExclusive(() =>
|
|
129
|
+
ctx.adapters.mount(body.id!, body.path!),
|
|
130
|
+
);
|
|
131
|
+
return Response.json({
|
|
132
|
+
id: info.id,
|
|
133
|
+
mounted_layers: info.mountedLayers,
|
|
134
|
+
rank: info.rank,
|
|
135
|
+
scale: info.scale,
|
|
136
|
+
ram_bytes: info.ramBytes,
|
|
137
|
+
});
|
|
138
|
+
} catch (error) {
|
|
139
|
+
return Response.json({ error: { message: (error as Error).message } }, { status: 400 });
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
case "adapters-unmount": {
|
|
144
|
+
const removed = await gateway.runExclusive(async () => ctx.adapters.unmount(route.id));
|
|
145
|
+
return removed > 0
|
|
146
|
+
? Response.json({ id: route.id, removed_layers: removed })
|
|
147
|
+
: Response.json(
|
|
148
|
+
{ error: { message: `adapter ${route.id} not mounted` } },
|
|
149
|
+
{ status: 404 },
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { runtimeValue } from "../runtime-config";
|
|
2
|
+
|
|
3
|
+
export const P2R_TRACE_PREFIX = "[p2r] ";
|
|
4
|
+
export const P2R_TRACE_VERSION = 1;
|
|
5
|
+
|
|
6
|
+
export type P2RTraceOutcome = "success" | "error" | "abort";
|
|
7
|
+
|
|
8
|
+
export type P2RTracePhase =
|
|
9
|
+
| "request.body_parse"
|
|
10
|
+
| "request.prompt_prepare"
|
|
11
|
+
| "completion.total"
|
|
12
|
+
| "completion.placement"
|
|
13
|
+
| "engine.admission_wait"
|
|
14
|
+
| "cache.lookup_restore"
|
|
15
|
+
| "prefill.total"
|
|
16
|
+
| "prefill.batch_setup"
|
|
17
|
+
| "prefill.chunk"
|
|
18
|
+
| "prefill.kv_maintenance"
|
|
19
|
+
| "token_zero.total"
|
|
20
|
+
| "token_zero.forward"
|
|
21
|
+
| "token_zero.head"
|
|
22
|
+
| "token_zero.sample"
|
|
23
|
+
| "response.first_write"
|
|
24
|
+
| "response.final_write";
|
|
25
|
+
|
|
26
|
+
export type P2RTraceAttributes = Readonly<Record<
|
|
27
|
+
string,
|
|
28
|
+
string | number | boolean | null
|
|
29
|
+
>>;
|
|
30
|
+
|
|
31
|
+
export interface P2RTraceEvent {
|
|
32
|
+
phase: P2RTracePhase;
|
|
33
|
+
startMs: number;
|
|
34
|
+
durationMs: number;
|
|
35
|
+
attributes?: P2RTraceAttributes;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface P2RTraceRecord {
|
|
39
|
+
version: typeof P2R_TRACE_VERSION;
|
|
40
|
+
traceId: string;
|
|
41
|
+
requestId: string;
|
|
42
|
+
route: string;
|
|
43
|
+
clock: "monotonic-ms";
|
|
44
|
+
outcome: P2RTraceOutcome;
|
|
45
|
+
totalMs: number;
|
|
46
|
+
events: P2RTraceEvent[];
|
|
47
|
+
attributes?: P2RTraceAttributes;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
type Clock = () => number;
|
|
51
|
+
type Emit = (record: P2RTraceRecord) => void;
|
|
52
|
+
|
|
53
|
+
export interface PromptResponseTraceOptions {
|
|
54
|
+
traceId: string;
|
|
55
|
+
requestId: string;
|
|
56
|
+
route: string;
|
|
57
|
+
clock?: Clock;
|
|
58
|
+
emit?: Emit;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Request-local diagnostic trace. Callers pass it explicitly because the
|
|
62
|
+
* continuous scheduler interleaves rows and cannot safely use ambient async
|
|
63
|
+
* context. No instance is created unless MLX_BUN_P2R_TRACE=1. */
|
|
64
|
+
export class PromptResponseTrace {
|
|
65
|
+
readonly #startedAt: number;
|
|
66
|
+
readonly #events: P2RTraceEvent[] = [];
|
|
67
|
+
readonly #clock: Clock;
|
|
68
|
+
readonly #emit: Emit;
|
|
69
|
+
#finished = false;
|
|
70
|
+
|
|
71
|
+
readonly traceId: string;
|
|
72
|
+
readonly requestId: string;
|
|
73
|
+
readonly route: string;
|
|
74
|
+
|
|
75
|
+
constructor(options: PromptResponseTraceOptions) {
|
|
76
|
+
this.traceId = options.traceId;
|
|
77
|
+
this.requestId = options.requestId;
|
|
78
|
+
this.route = options.route;
|
|
79
|
+
this.#clock = options.clock ?? performance.now.bind(performance);
|
|
80
|
+
this.#emit = options.emit ?? ((record) => {
|
|
81
|
+
console.error(`${P2R_TRACE_PREFIX}${JSON.stringify(record)}`);
|
|
82
|
+
});
|
|
83
|
+
this.#startedAt = this.#clock();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
get finished(): boolean {
|
|
87
|
+
return this.#finished;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Start one span. The returned closer is idempotent so finally blocks and
|
|
91
|
+
* early exits cannot double-record a phase. */
|
|
92
|
+
begin(phase: P2RTracePhase, attributes?: P2RTraceAttributes): () => void {
|
|
93
|
+
const startedAt = this.#clock();
|
|
94
|
+
let closed = false;
|
|
95
|
+
return () => {
|
|
96
|
+
if (closed || this.#finished) return;
|
|
97
|
+
closed = true;
|
|
98
|
+
const endedAt = this.#clock();
|
|
99
|
+
this.#events.push({
|
|
100
|
+
phase,
|
|
101
|
+
startMs: startedAt - this.#startedAt,
|
|
102
|
+
durationMs: endedAt - startedAt,
|
|
103
|
+
...(attributes ? { attributes } : {}),
|
|
104
|
+
});
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
mark(phase: P2RTracePhase, attributes?: P2RTraceAttributes): void {
|
|
109
|
+
if (this.#finished) return;
|
|
110
|
+
this.#events.push({
|
|
111
|
+
phase,
|
|
112
|
+
startMs: this.#clock() - this.#startedAt,
|
|
113
|
+
durationMs: 0,
|
|
114
|
+
...(attributes ? { attributes } : {}),
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
finish(
|
|
119
|
+
outcome: P2RTraceOutcome,
|
|
120
|
+
attributes?: P2RTraceAttributes,
|
|
121
|
+
): P2RTraceRecord | null {
|
|
122
|
+
if (this.#finished) return null;
|
|
123
|
+
this.#finished = true;
|
|
124
|
+
const totalMs = this.#clock() - this.#startedAt;
|
|
125
|
+
const record: P2RTraceRecord = {
|
|
126
|
+
version: P2R_TRACE_VERSION,
|
|
127
|
+
traceId: this.traceId,
|
|
128
|
+
requestId: this.requestId,
|
|
129
|
+
route: this.route,
|
|
130
|
+
clock: "monotonic-ms",
|
|
131
|
+
outcome,
|
|
132
|
+
totalMs,
|
|
133
|
+
events: [...this.#events].sort((a, b) => a.startMs - b.startMs),
|
|
134
|
+
...(attributes ? { attributes } : {}),
|
|
135
|
+
};
|
|
136
|
+
this.#emit(record);
|
|
137
|
+
return record;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function createPromptResponseTrace(
|
|
142
|
+
options: PromptResponseTraceOptions,
|
|
143
|
+
): PromptResponseTrace | undefined {
|
|
144
|
+
return runtimeValue("MLX_BUN_P2R_TRACE") === "1"
|
|
145
|
+
? new PromptResponseTrace(options)
|
|
146
|
+
: undefined;
|
|
147
|
+
}
|