mlx-bun 0.0.11 → 0.0.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/README.md +43 -13
  2. package/THIRD_PARTY_LICENSES.md +9 -0
  3. package/docs/design/colibri-glm52-port.md +1479 -0
  4. package/docs/design/pre-colibri-stabilization.md +329 -0
  5. package/docs/investigations/colibri-oracle-pin.md +1284 -0
  6. package/docs/investigations/indexshare-performance-spike.md +357 -0
  7. package/docs/planning/release-notes-v0.0.12.md +80 -0
  8. package/docs/reference/cli.md +28 -2
  9. package/docs/reference/distribution.md +25 -1
  10. package/docs/reference/embedding.md +5 -2
  11. package/docs/reference/features-matrix.md +4 -2
  12. package/docs/reference/library-api.md +41 -13
  13. package/docs/reference/models.md +49 -2
  14. package/docs/reference/server-api.md +90 -9
  15. package/docs/reference/server-config.md +21 -14
  16. package/docs/reference/training.md +10 -1
  17. package/package.json +1 -1
  18. package/src/anthropic.ts +54 -44
  19. package/src/chat-template.ts +155 -10
  20. package/src/cli.ts +127 -8
  21. package/src/config.ts +71 -26
  22. package/src/diffusion/diffusion-generate.ts +14 -8
  23. package/src/download.ts +103 -12
  24. package/src/eval/ifeval.ts +63 -120
  25. package/src/eval/runner.ts +14 -5
  26. package/src/eval/tasks/ifeval.ts +167 -31
  27. package/src/expert-io.ts +391 -0
  28. package/src/expert-residency.ts +1134 -0
  29. package/src/expert-usage.ts +415 -0
  30. package/src/generate.ts +114 -17
  31. package/src/hf-push.ts +1 -1
  32. package/src/index.ts +14 -2
  33. package/src/jobs/runner.ts +37 -18
  34. package/src/kv-store.ts +147 -8
  35. package/src/lora.ts +77 -14
  36. package/src/media-fetch.ts +198 -56
  37. package/src/mlx/array.ts +43 -0
  38. package/src/mlx/autograd.ts +51 -14
  39. package/src/mlx/checkpoint.ts +46 -13
  40. package/src/mlx/custom-vjp.ts +83 -35
  41. package/src/model/factory.ts +96 -5
  42. package/src/model/gemma4-base.ts +32 -0
  43. package/src/model/glm52-atlas.ts +511 -0
  44. package/src/model/glm52-cache.ts +706 -0
  45. package/src/model/glm52-config.ts +262 -0
  46. package/src/model/glm52-container.ts +305 -0
  47. package/src/model/glm52-coupling.ts +388 -0
  48. package/src/model/glm52-dsa.ts +602 -0
  49. package/src/model/glm52-expert-layout.ts +253 -0
  50. package/src/model/glm52-layout.ts +152 -0
  51. package/src/model/glm52-memory.ts +473 -0
  52. package/src/model/glm52-mla.ts +786 -0
  53. package/src/model/glm52-moe.ts +223 -0
  54. package/src/model/glm52-pilot.ts +537 -0
  55. package/src/model/glm52-reference.ts +445 -0
  56. package/src/model/glm52-residency.ts +557 -0
  57. package/src/model/glm52-resident-weights.ts +135 -0
  58. package/src/model/glm52-streamed-experts.ts +453 -0
  59. package/src/model/glm52-streamed-metal.ts +610 -0
  60. package/src/model/glm52-weights.ts +503 -0
  61. package/src/model/glm52.ts +1153 -0
  62. package/src/model/support.ts +8 -1
  63. package/src/native/expert_io.c +523 -0
  64. package/src/native-pack.ts +6 -4
  65. package/src/responses.ts +62 -49
  66. package/src/serve/batch-scheduler.ts +95 -16
  67. package/src/serve/generation-gateway.ts +41 -16
  68. package/src/server.ts +362 -81
  69. package/src/spec/glm52-mtp-source.ts +392 -0
  70. package/src/spec/serve-loop.ts +99 -18
  71. package/src/spec/source.ts +31 -6
  72. package/src/ssd-cache.ts +6 -2
  73. package/src/tool-call.ts +30 -2
  74. package/src/train/job.ts +0 -10
  75. package/src/train/lora-params.ts +17 -6
  76. package/src/train/trainer.ts +59 -26
  77. package/src/web/app.html +13 -2
  78. package/src/web/app.js +30 -3
  79. package/src/web/src/status.ts +51 -3
  80. package/src/web-tools.ts +54 -2
  81. package/src/weights.ts +82 -19
package/src/download.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  // lore"): Xet stalls (we speak plain HTTPS resolve/CDN), no resume on
5
5
  // flaky links (we Range-resume partial blobs), and silent corruption
6
6
  // (every blob is verified — sha256 for LFS files against the API's
7
- // oid, git-blob sha1 for small files against blobId).
7
+ // normalized LFS digest, git-blob sha1 for small files against blobId).
8
8
  //
9
9
  // Writes the exact huggingface_hub cache layout so the registry and
10
10
  // loaders find the result with zero changes:
@@ -18,6 +18,7 @@
18
18
  import {
19
19
  mkdirSync, existsSync, statSync, createWriteStream, renameSync, rmSync,
20
20
  symlinkSync, readFileSync, openSync, closeSync, readSync, writeSync,
21
+ statfsSync,
21
22
  } from "node:fs";
22
23
  import { dirname, join } from "node:path";
23
24
  import { DEFAULT_HUB } from "./registry";
@@ -29,8 +30,8 @@ export interface RepoFile {
29
30
  size: number;
30
31
  /** git blob sha1 (non-LFS identity, and the blob filename for non-LFS). */
31
32
  blobId: string;
32
- /** LFS sha256 + size; present for large files. oid is the blob filename. */
33
- lfs?: { oid: string; size: number };
33
+ /** LFS sha256 + size; present for large files. sha256 is the blob filename. */
34
+ lfs?: { sha256: string; size: number };
34
35
  }
35
36
 
36
37
  export interface RepoListing {
@@ -47,6 +48,56 @@ export interface DownloadOptions {
47
48
  onProgress?: (file: string, received: number, total: number) => void;
48
49
  }
49
50
 
51
+ export interface DownloadSpacePlan {
52
+ /** Bytes still absent after crediting verified blobs and resumable prefixes. */
53
+ remainingBytes: number;
54
+ /** Files that still need at least one byte. */
55
+ remainingFiles: number;
56
+ /** Files already complete in the shared HF blob store. */
57
+ completeFiles: number;
58
+ /** Existing `.incomplete` bytes that will be rehashed and resumed. */
59
+ resumableBytes: number;
60
+ }
61
+
62
+ /** One GiB for filesystem metadata, the final refs write, and normal host
63
+ * operation. The artifact bytes themselves are exact; this is deliberately a
64
+ * small fixed guard rather than a percentage that would add tens of GiB to a
65
+ * Colibri download. */
66
+ export const DOWNLOAD_DISK_RESERVE_BYTES = 2 ** 30;
67
+
68
+ /** Compute the exact remaining payload without opening or hashing any blob.
69
+ * A valid-size final blob is already complete; an in-range `.incomplete`
70
+ * prefix is credited because downloadOneLocked resumes it in place. */
71
+ export function planDownloadSpace(
72
+ files: readonly RepoFile[],
73
+ blobsDir: string,
74
+ ): DownloadSpacePlan {
75
+ let remainingBytes = 0;
76
+ let remainingFiles = 0;
77
+ let completeFiles = 0;
78
+ let resumableBytes = 0;
79
+ for (const file of files) {
80
+ const blobId = file.lfs?.sha256 ?? file.blobId;
81
+ if (!blobId) throw new Error(`no blob id for ${file.rfilename}`);
82
+ const blobPath = join(blobsDir, blobId);
83
+ if (existsSync(blobPath) && statSync(blobPath).size === file.size) {
84
+ completeFiles++;
85
+ continue;
86
+ }
87
+ const partPath = `${blobPath}.incomplete`;
88
+ const partBytes = existsSync(partPath) ? statSync(partPath).size : 0;
89
+ const credited = partBytes >= 0 && partBytes <= file.size ? partBytes : 0;
90
+ resumableBytes += credited;
91
+ remainingBytes += file.size - credited;
92
+ remainingFiles++;
93
+ }
94
+ return { remainingBytes, remainingFiles, completeFiles, resumableBytes };
95
+ }
96
+
97
+ function formatGib(bytes: number): string {
98
+ return `${(bytes / 2 ** 30).toFixed(2)} GiB`;
99
+ }
100
+
50
101
  /** HF auth: explicit > env > the file `hf auth login` writes. */
51
102
  export function hfToken(): string | null {
52
103
  if (process.env.HF_TOKEN) return process.env.HF_TOKEN;
@@ -161,16 +212,34 @@ export async function listRepoFiles(
161
212
  );
162
213
  const body = (await res.json()) as {
163
214
  sha: string;
164
- siblings: { rfilename: string; size?: number; blobId?: string; lfs?: { oid: string; size: number } }[];
215
+ siblings: {
216
+ rfilename: string;
217
+ size?: number;
218
+ blobId?: string;
219
+ // The current HF API calls this field `sha256`. Accept the older `oid`
220
+ // spelling too, but normalize it before the download path sees it.
221
+ lfs?: { sha256?: string; oid?: string; size: number };
222
+ }[];
165
223
  };
166
224
  return {
167
225
  sha: body.sha,
168
- files: body.siblings.map((s) => ({
169
- rfilename: s.rfilename,
170
- size: s.lfs?.size ?? s.size ?? 0,
171
- blobId: s.blobId ?? "",
172
- lfs: s.lfs,
173
- })),
226
+ files: body.siblings.map((s) => {
227
+ let lfs: RepoFile["lfs"];
228
+ if (s.lfs) {
229
+ const sha256 = s.lfs.sha256 ?? s.lfs.oid;
230
+ if (!sha256 || !/^[0-9a-f]{64}$/i.test(sha256))
231
+ throw new Error(`HF API returned an invalid LFS sha256 for ${s.rfilename}`);
232
+ lfs = { sha256: sha256.toLowerCase(), size: s.lfs.size };
233
+ }
234
+ if (!lfs && (!s.blobId || !/^[0-9a-f]{40}$/.test(s.blobId)))
235
+ throw new Error(`HF API returned an invalid git blob SHA-1 for ${s.rfilename}`);
236
+ return {
237
+ rfilename: s.rfilename,
238
+ size: lfs?.size ?? s.size ?? 0,
239
+ blobId: s.blobId ?? "",
240
+ lfs,
241
+ };
242
+ }),
174
243
  };
175
244
  }
176
245
 
@@ -349,6 +418,28 @@ export async function downloadModel(
349
418
  mkdirSync(blobsDir, { recursive: true });
350
419
  mkdirSync(join(repoDir, "refs"), { recursive: true });
351
420
 
421
+ // Preflight before the first network payload. This matters for direct
422
+ // Colibri artifacts (hundreds of GiB): discovering a full volume after the
423
+ // 140th shard is not a usable failure mode. Existing verified blobs and
424
+ // partial prefixes are credited exactly, so rerunning after interruption is
425
+ // both resumable and progressively cheaper.
426
+ const space = planDownloadSpace(listing.files, blobsDir);
427
+ if (space.remainingBytes > 0) {
428
+ const fs = statfsSync(blobsDir);
429
+ const availableBytes = fs.bavail * fs.bsize;
430
+ const requiredBytes = space.remainingBytes + DOWNLOAD_DISK_RESERVE_BYTES;
431
+ if (requiredBytes > availableBytes) {
432
+ throw new Error(
433
+ `insufficient disk space for ${repoId}: ${formatGib(space.remainingBytes)} ` +
434
+ `remains across ${space.remainingFiles} files, but only ` +
435
+ `${formatGib(availableBytes)} is available in ${hub} ` +
436
+ `(${formatGib(DOWNLOAD_DISK_RESERVE_BYTES)} safety reserve). ` +
437
+ `Free space or choose another cache directory, then rerun; ` +
438
+ `${formatGib(space.resumableBytes)} of partial data will resume.`,
439
+ );
440
+ }
441
+ }
442
+
352
443
  const status: DownloadStatus = {
353
444
  repoId, state: "active", currentFile: null,
354
445
  receivedBytes: 0, totalBytes: listing.files.reduce((a, f) => a + f.size, 0),
@@ -369,7 +460,7 @@ export async function downloadModel(
369
460
 
370
461
  try {
371
462
  for (const f of listing.files) {
372
- const blobId = f.lfs?.oid ?? f.blobId;
463
+ const blobId = f.lfs?.sha256 ?? f.blobId;
373
464
  if (!blobId) throw new Error(`no blob id for ${f.rfilename} (API response missing ?blobs=true data)`);
374
465
  const blobPath = join(blobsDir, blobId);
375
466
 
@@ -378,7 +469,7 @@ export async function downloadModel(
378
469
  status.currentFile = f.rfilename;
379
470
  await downloadOne(url, token, blobPath, {
380
471
  size: f.size, name: f.rfilename,
381
- ...(f.lfs ? { sha256: f.lfs.oid } : { sha1: f.blobId }),
472
+ ...(f.lfs ? { sha256: f.lfs.sha256 } : { sha1: f.blobId }),
382
473
  }, (file, received, total) => {
383
474
  status.receivedBytes = doneBytes + received;
384
475
  sampleRate(status.receivedBytes);
@@ -1,138 +1,81 @@
1
- // IFEval — programmatically-verifiable instruction-following scoring (Zhou et
2
- // al. 2023, "Instruction-Following Evaluation for Large Language Models"). Each
3
- // instance pairs a prompt with one or more *verifiable* instructions; a response
4
- // is scored by deterministic checks (no LLM judge), so before/after ORPO
5
- // comparisons are exact and reproducible.
1
+ // Compatibility facade for the canonical IFEval scorer.
6
2
  //
7
- // This implements the high-frequency instruction families (a working subset of
8
- // the ~25 in the reference suite, extensible via INSTRUCTIONS). Strict scoring:
9
- // an instance passes (strict) iff every instruction passes; we also report the
10
- // per-instruction pass rate. The official metrics are prompt-level and
11
- // instruction-level accuracy — both derived from `scoreInstance`.
3
+ // The complete verifier registry and strict/loose aggregation contract live in
4
+ // `tasks/ifeval.ts`, which is also used by `scripts/eval.ts`. Keep this module
5
+ // as the stable import surface used by `scripts/run-ifeval.ts` and older
6
+ // callers; it must not grow a second verifier implementation.
12
7
 
13
- export interface IFEvalInstance {
14
- /** The prompt text (used by instructions that reference it, e.g. "repeat the request"). */
15
- prompt: string;
16
- /** Instruction ids, e.g. "keywords:existence". */
17
- instruction_id_list: string[];
18
- /** Per-instruction kwargs, aligned with instruction_id_list. */
19
- kwargs: Array<Record<string, unknown>>;
20
- }
8
+ import {
9
+ scoreIfevalInstance,
10
+ scoreIfevalPairs,
11
+ type IfevalInstance,
12
+ type IfevalResult,
13
+ } from "./tasks/ifeval";
14
+
15
+ export {
16
+ SUPPORTED_INSTRUCTIONS,
17
+ looseClean,
18
+ scoreIfevalInstance,
19
+ scoreIfevalPairs,
20
+ stripThinking,
21
+ verifyResponse,
22
+ type IfevalCoverage,
23
+ type IfevalInstance,
24
+ type IfevalInstanceScore,
25
+ type IfevalPair,
26
+ type IfevalResult,
27
+ type IfevalVerification,
28
+ } from "./tasks/ifeval";
29
+
30
+ /** Historical spelling retained for callers of this module. */
31
+ export type IFEvalInstance = IfevalInstance;
21
32
 
22
33
  export interface InstanceResult {
23
- /** One pass/fail per instruction (aligned with instruction_id_list). */
34
+ /** Unknown instructions are true here per the canonical prompt contract. */
24
35
  perInstruction: boolean[];
25
- /** Strict: all instructions passed. */
26
36
  followedAll: boolean;
37
+ /** Consult this field rather than interpreting unknown IDs as verified. */
38
+ unhandled: string[];
27
39
  }
28
40
 
29
- type Checker = (response: string, kw: Record<string, unknown>, inst: IFEvalInstance) => boolean;
30
-
31
- const words = (s: string): string[] => s.trim().split(/\s+/).filter(Boolean);
32
- const sentences = (s: string): string[] =>
33
- s.split(/(?<=[.!?])\s+/).map((x) => x.trim()).filter(Boolean);
34
- const paragraphs = (s: string): string[] =>
35
- s.split(/\n\s*\n/).map((x) => x.trim()).filter(Boolean);
36
- const norm = (s: string): string => s.toLowerCase();
37
-
38
- const cmp = (n: number, rel: string | undefined, target: number): boolean => {
39
- switch (rel) {
40
- case "at least": return n >= target;
41
- case "at most": return n <= target;
42
- case "less than": return n < target;
43
- case "exactly": return n === target;
44
- default: return n >= target; // IFEval default for "around"/unspecified is lenient ≥
45
- }
46
- };
47
-
48
- /** The verifiable-instruction registry. Keyed by IFEval instruction_id. */
49
- export const INSTRUCTIONS: Record<string, Checker> = {
50
- // ---- keywords ----
51
- "keywords:existence": (r, kw) =>
52
- (kw.keywords as string[]).every((k) => norm(r).includes(norm(k))),
53
- "keywords:frequency": (r, kw) => {
54
- const k = norm(kw.keyword as string);
55
- const count = norm(r).split(k).length - 1;
56
- return cmp(count, kw.relation as string, kw.frequency as number);
57
- },
58
- "keywords:forbidden_words": (r, kw) =>
59
- !(kw.forbidden_words as string[]).some((k) => norm(r).includes(norm(k))),
60
-
61
- // ---- length constraints ----
62
- "length_constraints:number_words": (r, kw) =>
63
- cmp(words(r).length, kw.relation as string, kw.num_words as number),
64
- "length_constraints:number_sentences": (r, kw) =>
65
- cmp(sentences(r).length, kw.relation as string, kw.num_sentences as number),
66
- "length_constraints:number_paragraphs": (r, kw) =>
67
- cmp(paragraphs(r).length, kw.relation as string, kw.num_paragraphs as number),
68
-
69
- // ---- detectable format ----
70
- "detectable_format:number_bullet_lists": (r, kw) => {
71
- const bullets = r.split("\n").filter((l) => /^\s*[*\-]\s+/.test(l)).length;
72
- return cmp(bullets, kw.relation as string, kw.num_bullets as number);
73
- },
74
- "detectable_format:number_highlighted_sections": (r, kw) => {
75
- const hi = (r.match(/\*[^*\n]+\*/g) ?? []).length;
76
- return cmp(hi, kw.relation as string, kw.num_highlights as number);
77
- },
78
- "detectable_format:title": (r) => /<<[^>\n]+>>/.test(r),
79
- "detectable_format:json_format": (r) => {
80
- const t = r.trim().replace(/^```(?:json)?\s*|\s*```$/g, "").trim();
81
- try { JSON.parse(t); return true; } catch { return false; }
82
- },
83
-
84
- // ---- case ----
85
- "change_case:english_lowercase": (r) => r === r.toLowerCase(),
86
- "change_case:english_capital": (r) => r === r.toUpperCase(),
87
-
88
- // ---- start / end ----
89
- "startend:end_checker": (r, kw) =>
90
- norm(r.trimEnd()).endsWith(norm((kw.end_phrase as string).trim())),
91
- "startend:quotation": (r) => {
92
- const t = r.trim();
93
- return t.startsWith('"') && t.endsWith('"') && t.length >= 2;
94
- },
95
-
96
- // ---- punctuation ----
97
- "punctuation:no_comma": (r) => !r.includes(","),
98
- };
99
-
100
- /** Score one IFEval instance (strict). Unknown instruction ids count as failed
101
- * (so coverage gaps never inflate the score) — filter the dataset to the
102
- * supported ids, or extend INSTRUCTIONS, to avoid that. */
103
- export function scoreInstance(instance: IFEvalInstance, response: string): InstanceResult {
104
- const perInstruction = instance.instruction_id_list.map((id, i) => {
105
- const check = INSTRUCTIONS[id];
106
- if (!check) return false;
107
- try { return check(response, instance.kwargs[i] ?? {}, instance); } catch { return false; }
108
- });
109
- return { perInstruction, followedAll: perInstruction.every(Boolean) };
41
+ /**
42
+ * Historical strict-only helper, now delegated to the canonical scorer.
43
+ * New code should prefer `scoreIfevalInstance` for both strict and loose modes.
44
+ */
45
+ export function scoreInstance(
46
+ instance: IFEvalInstance,
47
+ response: string,
48
+ ): InstanceResult {
49
+ const { strict } = scoreIfevalInstance(instance, response);
50
+ return {
51
+ perInstruction: strict.instructionPasses.map((result) => result ?? true),
52
+ followedAll: strict.pass,
53
+ unhandled: strict.unhandled,
54
+ };
110
55
  }
111
56
 
112
- export interface IFEvalReport {
57
+ export interface IFEvalReport extends IfevalResult {
58
+ /** Historical alias for nTotal. */
113
59
  n: number;
114
- /** Fraction of instances where ALL instructions were followed. */
60
+ /** Historical alias for strictAcc (prompt-level strict). */
115
61
  promptAccuracy: number;
116
- /** Fraction of individual instructions followed. */
62
+ /** Historical alias for strictInstructionAcc. */
117
63
  instructionAccuracy: number;
118
64
  }
119
65
 
120
- /** Aggregate the official prompt-level + instruction-level accuracies over a set
121
- * of (instance, response) pairs. */
122
- export function aggregate(pairs: Array<{ instance: IFEvalInstance; response: string }>): IFEvalReport {
123
- let prompts = 0, instrFollowed = 0, instrTotal = 0;
124
- for (const { instance, response } of pairs) {
125
- const r = scoreInstance(instance, response);
126
- if (r.followedAll) prompts++;
127
- instrFollowed += r.perInstruction.filter(Boolean).length;
128
- instrTotal += r.perInstruction.length;
129
- }
66
+ /**
67
+ * Historical aggregation helper with unambiguous canonical fields plus aliases.
68
+ * Unknown instructions do not fail prompt accuracy, are excluded from
69
+ * instruction accuracy, and remain visible in `coverage`.
70
+ */
71
+ export function aggregate(
72
+ pairs: Array<{ instance: IFEvalInstance; response: string }>,
73
+ ): IFEvalReport {
74
+ const result = scoreIfevalPairs(pairs);
130
75
  return {
131
- n: pairs.length,
132
- promptAccuracy: pairs.length ? prompts / pairs.length : 0,
133
- instructionAccuracy: instrTotal ? instrFollowed / instrTotal : 0,
76
+ ...result,
77
+ n: result.nTotal,
78
+ promptAccuracy: result.strictAcc,
79
+ instructionAccuracy: result.strictInstructionAcc,
134
80
  };
135
81
  }
136
-
137
- /** Instruction ids this implementation supports (filter datasets to these). */
138
- export const SUPPORTED_INSTRUCTIONS = new Set(Object.keys(INSTRUCTIONS));
@@ -9,8 +9,12 @@ import { generate, type GenerateOptions } from "../generate";
9
9
  import * as ops from "../mlx/ops";
10
10
  import { ChatTemplate } from "../chat-template";
11
11
  import { loadModelConfig, type ModelConfig } from "../config";
12
- import { Weights } from "../weights";
13
- import { createModel, type RuntimeModel } from "../model/factory";
12
+ import {
13
+ openModel,
14
+ type Glm52RuntimeOpenOptions,
15
+ type RuntimeModel,
16
+ } from "../model/factory";
17
+ import { isGlm52Config } from "../model/support";
14
18
  import { DiffusionGemmaModel } from "../model/diffusion-gemma";
15
19
  import { loadTokenizer, type LoadedTokenizer } from "../tokenizer";
16
20
  import { resolveModelDir } from "./kl";
@@ -65,11 +69,16 @@ export interface TaskModel {
65
69
  activeAdapters?: string[];
66
70
  }
67
71
 
68
- export async function loadTaskModel(query: string, adapterDir?: string): Promise<TaskModel> {
72
+ export async function loadTaskModel(
73
+ query: string,
74
+ adapterDir?: string,
75
+ runtimeOptions: Glm52RuntimeOpenOptions = {},
76
+ ): Promise<TaskModel> {
69
77
  const dir = resolveModelDir(query);
70
78
  const config = await loadModelConfig(dir);
71
- const weights = await Weights.open(dir);
72
- const model = createModel(weights, config);
79
+ if (adapterDir && isGlm52Config(config))
80
+ throw new Error("GLM-5.2 does not support LoRA adapters");
81
+ const model = await openModel(dir, runtimeOptions);
73
82
  const activeAdapters: string[] = [];
74
83
  if (adapterDir) {
75
84
  // Mount a trained LoRA adapter so the eval measures base+adapter (e.g. the