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.
- package/README.md +43 -13
- package/THIRD_PARTY_LICENSES.md +9 -0
- package/docs/design/colibri-glm52-port.md +1479 -0
- package/docs/design/pre-colibri-stabilization.md +329 -0
- package/docs/investigations/colibri-oracle-pin.md +1284 -0
- package/docs/investigations/indexshare-performance-spike.md +357 -0
- package/docs/planning/release-notes-v0.0.12.md +80 -0
- package/docs/reference/cli.md +28 -2
- package/docs/reference/distribution.md +25 -1
- package/docs/reference/embedding.md +5 -2
- package/docs/reference/features-matrix.md +4 -2
- package/docs/reference/library-api.md +41 -13
- package/docs/reference/models.md +49 -2
- package/docs/reference/server-api.md +90 -9
- package/docs/reference/server-config.md +21 -14
- package/docs/reference/training.md +10 -1
- package/package.json +1 -1
- package/src/anthropic.ts +54 -44
- package/src/chat-template.ts +155 -10
- package/src/cli.ts +127 -8
- package/src/config.ts +71 -26
- package/src/diffusion/diffusion-generate.ts +14 -8
- package/src/download.ts +103 -12
- package/src/eval/ifeval.ts +63 -120
- package/src/eval/runner.ts +14 -5
- package/src/eval/tasks/ifeval.ts +167 -31
- package/src/expert-io.ts +391 -0
- package/src/expert-residency.ts +1134 -0
- package/src/expert-usage.ts +415 -0
- package/src/generate.ts +114 -17
- package/src/hf-push.ts +1 -1
- package/src/index.ts +14 -2
- package/src/jobs/runner.ts +37 -18
- package/src/kv-store.ts +147 -8
- package/src/lora.ts +77 -14
- package/src/media-fetch.ts +198 -56
- package/src/mlx/array.ts +43 -0
- package/src/mlx/autograd.ts +51 -14
- package/src/mlx/checkpoint.ts +46 -13
- package/src/mlx/custom-vjp.ts +83 -35
- package/src/model/factory.ts +96 -5
- package/src/model/gemma4-base.ts +32 -0
- package/src/model/glm52-atlas.ts +511 -0
- package/src/model/glm52-cache.ts +706 -0
- package/src/model/glm52-config.ts +262 -0
- package/src/model/glm52-container.ts +305 -0
- package/src/model/glm52-coupling.ts +388 -0
- package/src/model/glm52-dsa.ts +602 -0
- package/src/model/glm52-expert-layout.ts +253 -0
- package/src/model/glm52-layout.ts +152 -0
- package/src/model/glm52-memory.ts +473 -0
- package/src/model/glm52-mla.ts +786 -0
- package/src/model/glm52-moe.ts +223 -0
- package/src/model/glm52-pilot.ts +537 -0
- package/src/model/glm52-reference.ts +445 -0
- package/src/model/glm52-residency.ts +557 -0
- package/src/model/glm52-resident-weights.ts +135 -0
- package/src/model/glm52-streamed-experts.ts +453 -0
- package/src/model/glm52-streamed-metal.ts +610 -0
- package/src/model/glm52-weights.ts +503 -0
- package/src/model/glm52.ts +1153 -0
- package/src/model/support.ts +8 -1
- package/src/native/expert_io.c +523 -0
- package/src/native-pack.ts +6 -4
- package/src/responses.ts +62 -49
- package/src/serve/batch-scheduler.ts +95 -16
- package/src/serve/generation-gateway.ts +41 -16
- package/src/server.ts +362 -81
- package/src/spec/glm52-mtp-source.ts +392 -0
- package/src/spec/serve-loop.ts +99 -18
- package/src/spec/source.ts +31 -6
- package/src/ssd-cache.ts +6 -2
- package/src/tool-call.ts +30 -2
- package/src/train/job.ts +0 -10
- package/src/train/lora-params.ts +17 -6
- package/src/train/trainer.ts +59 -26
- package/src/web/app.html +13 -2
- package/src/web/app.js +30 -3
- package/src/web/src/status.ts +51 -3
- package/src/web-tools.ts +54 -2
- package/src/weights.ts +82 -19
|
@@ -16,9 +16,7 @@ surface); direct library callers must do the same.
|
|
|
16
16
|
```ts
|
|
17
17
|
import {
|
|
18
18
|
ensureNativeRuntime,
|
|
19
|
-
|
|
20
|
-
Weights,
|
|
21
|
-
createModel, // dispatches to Gemma4Model / MiniCPM5Model / Qwen35Model
|
|
19
|
+
openModel, // artifact-aware dispatch, including bounded GLM-5.2
|
|
22
20
|
loadTokenizer,
|
|
23
21
|
ChatTemplate,
|
|
24
22
|
generate,
|
|
@@ -26,13 +24,15 @@ import {
|
|
|
26
24
|
|
|
27
25
|
await ensureNativeRuntime();
|
|
28
26
|
const dir = "/path/to/hf-snapshot"; // mlx-bun ls prints these
|
|
29
|
-
const
|
|
30
|
-
|
|
27
|
+
const model = await openModel(dir, {
|
|
28
|
+
// Used only by the direct Colibri GLM-5.2 artifact; other models ignore it.
|
|
29
|
+
contextTokens: 4096, maxGenerationTokens: 256, enableMtp: false,
|
|
30
|
+
});
|
|
31
31
|
const tok = await loadTokenizer(dir);
|
|
32
32
|
const template = await ChatTemplate.load(dir);
|
|
33
33
|
// Gemma4Model and MiniCPM5Model can still be imported directly from "mlx-bun"
|
|
34
34
|
// if you need the concrete type. Qwen35Model is NOT exported from the public
|
|
35
|
-
// package — use
|
|
35
|
+
// package — use openModel/RuntimeModel (preferred artifact-aware dispatch), or import from
|
|
36
36
|
// "./src/model/qwen3_5" in-repo.
|
|
37
37
|
|
|
38
38
|
const ids = tok.encode(template.render([{ role: "user", content: "hi" }]));
|
|
@@ -45,13 +45,15 @@ console.log(tok.decode(out, true));
|
|
|
45
45
|
console.log(gen.stats); // set once iteration ends
|
|
46
46
|
```
|
|
47
47
|
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
Ordinary safetensors loading is lazy (mmap + mlx native loader). GLM-5.2 first
|
|
49
|
+
runs its header-only exact process equation, then opens the bounded Colibri
|
|
50
|
+
resident/expert tiers; impossible plans fail before either tier is committed.
|
|
50
51
|
|
|
51
52
|
> **First run:** library consumers bypass the CLI's first-run step, so
|
|
52
53
|
> call `await ensureNativeRuntime()` once before constructing a model on
|
|
53
54
|
> a machine that may not have the MLX native runtime yet — it downloads
|
|
54
|
-
> the sha256-verified native pack
|
|
55
|
+
> the sha256-verified native pack (MLX plus the direct-Colibri expert-I/O
|
|
56
|
+
> helper) to `~/Library/Caches/mlx-bun/` and is
|
|
55
57
|
> a no-op when the runtime is already present (beside the executable,
|
|
56
58
|
> already cached, or installed via homebrew). `nativeRuntimeDir()`
|
|
57
59
|
> returns the resolved directory, or `null` on a fresh machine.
|
|
@@ -109,6 +111,24 @@ budget (pre-GPU, mmap-only check). The server exposes OpenAI chat
|
|
|
109
111
|
completions, Anthropic `/v1/messages`, OpenAI Responses, embeddings,
|
|
110
112
|
adapters, and `/stats` — [server-api.md](./server-api.md).
|
|
111
113
|
|
|
114
|
+
For GLM-5.2, pass its load-only resource controls under `glm`:
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
const ctx = await loadContext(dir, "glm-5.2", {
|
|
118
|
+
glm: {
|
|
119
|
+
contextTokens: 4096,
|
|
120
|
+
maxGenerationTokens: 128,
|
|
121
|
+
batchSize: 8,
|
|
122
|
+
enableMtp: true, // default; native MTP routes requests serial+spec
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
`openModel()` and `loadContext()` both select the streamed Colibri runtime;
|
|
128
|
+
callers do not use `Weights.open()` or `createModel()` for this artifact.
|
|
129
|
+
Direct `generate()` supports the full sampling/grammar/logprobs contract but
|
|
130
|
+
does not itself mount a drafter; native MTP is a serving-context feature.
|
|
131
|
+
|
|
112
132
|
## Text embeddings
|
|
113
133
|
|
|
114
134
|
```ts
|
|
@@ -154,16 +174,24 @@ under one adapter must not seed another's prefill).
|
|
|
154
174
|
```ts
|
|
155
175
|
import { saveKvCache, loadKvCache } from "./src/kv-store";
|
|
156
176
|
saveKvCache("/tmp/prefix.kv", tokens, caches); // page-aligned file
|
|
157
|
-
const { tokens, caches
|
|
158
|
-
|
|
159
|
-
|
|
177
|
+
const { tokens, caches } = loadKvCache("/tmp/prefix.kv", model, {
|
|
178
|
+
modelId, configFingerprint, tokenizerHash, verify: true,
|
|
179
|
+
});
|
|
180
|
+
// Restore streams one tensor at a time from a read-only mmap into MLX-owned
|
|
181
|
+
// storage, drops each clean file-page range, and unmaps before returning.
|
|
160
182
|
// Note: loadKvCache accepts anything with makeCache(): Cache[] — it validates
|
|
161
183
|
// the entry count against the cache list makeCache() returns (not
|
|
162
184
|
// model.layers.length, which is wrong for KV-shared models like e4b, whose
|
|
163
185
|
// makeCache() returns donors only). RuntimeModel qualifies directly, no cast.
|
|
164
186
|
```
|
|
165
187
|
|
|
166
|
-
|
|
188
|
+
Format v3 supports plain, rotating, quantized, TurboQuant, SSM, and GLM-5.2
|
|
189
|
+
checkpoint-native caches. GLM rows use `mla`, `mla-dsa`, and `mtp-mla` kinds and
|
|
190
|
+
store only compressed latent/RoPE/DSA state—never reconstructed full K/V.
|
|
191
|
+
`Glm52NativeMtpSource.clonePersistentCache()` returns an owned MTP snapshot;
|
|
192
|
+
`restorePersistentCache()` adopts a validated restored `mtp-mla` row. Writes
|
|
193
|
+
are page-aligned, hashed, atomic (`.tmp` + fsync + rename), and available in
|
|
194
|
+
both synchronous and event-loop-yielding `saveKvCacheAsync` forms.
|
|
167
195
|
|
|
168
196
|
## Registry + fit (model discovery and memory math)
|
|
169
197
|
|
package/docs/reference/models.md
CHANGED
|
@@ -35,6 +35,11 @@ mlx-bun get <substring> # no "/" = registry query, re-gets the match
|
|
|
35
35
|
- Plain HTTPS resolve/CDN (no Xet), sequential files, `Range`-resume of
|
|
36
36
|
partial blobs (`<blob>.incomplete`). Auth: `HF_TOKEN` env, then the
|
|
37
37
|
token `hf auth login` writes. 401/403 answers get a "gated repo" hint.
|
|
38
|
+
- Before the first payload request, mlx-bun totals the bytes still absent,
|
|
39
|
+
credits complete shared blobs and valid `.incomplete` prefixes, and checks
|
|
40
|
+
the target volume for that remainder plus a fixed 1 GiB safety reserve. An
|
|
41
|
+
impossible acquisition fails immediately with required/available GiB and a
|
|
42
|
+
rerun/resume instruction rather than filling the disk halfway through.
|
|
38
43
|
- A substring argument (no `/`) resolves against already-downloaded
|
|
39
44
|
repos — `mlx-bun get 12B` refreshes the 12B to upstream's latest. An
|
|
40
45
|
unknown substring errors with a pointer to `mlx-bun ls <q>`.
|
|
@@ -51,14 +56,56 @@ mlx-bun get <substring> # no "/" = registry query, re-gets the match
|
|
|
51
56
|
Every blob is checksummed **while streaming** (a resume re-hashes the
|
|
52
57
|
existing prefix in chunks — no whole-file allocation):
|
|
53
58
|
|
|
54
|
-
- **LFS files** (weights): sha256 must equal the API's `lfs.
|
|
55
|
-
is also
|
|
59
|
+
- **LFS files** (weights): sha256 must equal the API's current `lfs.sha256`
|
|
60
|
+
field (the older `lfs.oid` spelling is also accepted) — which is also the
|
|
61
|
+
blob's filename.
|
|
56
62
|
- **Small files** (configs, tokenizer): git blob identity,
|
|
57
63
|
`sha1("blob <size>\0" + content)`, must equal the API's `blobId`.
|
|
58
64
|
|
|
59
65
|
A mismatch deletes the partial (never resume corrupt bytes); a short
|
|
60
66
|
read keeps the `.incomplete` for resume.
|
|
61
67
|
|
|
68
|
+
## GLM-5.2 on a 32 GB Mac
|
|
69
|
+
|
|
70
|
+
The validated direct-container artifact is
|
|
71
|
+
`mateogrgic/GLM-5.2-colibri-int4-with-int8-mtp` at revision
|
|
72
|
+
`3cc8db99b1b13fc79325d987ba3c1c430766b3b8`. It is approximately 384 GB
|
|
73
|
+
decimal / 357 GiB on disk, so choose the Hugging Face cache volume before the
|
|
74
|
+
download. Fast internal NVMe is strongly preferred; an external volume also
|
|
75
|
+
needs to sustain the expert-streaming workload after acquisition.
|
|
76
|
+
|
|
77
|
+
```sh
|
|
78
|
+
# Optional: choose a large fast volume before downloading.
|
|
79
|
+
export HF_HUB_CACHE=/Volumes/FastSSD/huggingface/hub
|
|
80
|
+
|
|
81
|
+
mlx-bun get mateogrgic/GLM-5.2-colibri-int4-with-int8-mtp \
|
|
82
|
+
--revision 3cc8db99b1b13fc79325d987ba3c1c430766b3b8
|
|
83
|
+
mlx-bun scan
|
|
84
|
+
mlx-bun serve GLM-5.2-colibri \
|
|
85
|
+
--memory-budget 26.8435456 \
|
|
86
|
+
--context-length 4096 \
|
|
87
|
+
--max-tokens 128 \
|
|
88
|
+
--mtp on
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
`26.8435456` decimal GB is exactly 25 GiB. The validated plan reserves about
|
|
92
|
+
19.9 GiB for the process and leaves the rest of the 32 GiB machine to macOS.
|
|
93
|
+
Startup runs the exact header-only resource equation before opening resident
|
|
94
|
+
weights or expert slabs. The server reports the resulting plan at
|
|
95
|
+
`GET /stats` under `glm52`.
|
|
96
|
+
|
|
97
|
+
If acquisition is interrupted, rerun the identical `get` command: mlx-bun
|
|
98
|
+
rehashes each existing prefix in bounded chunks, sends a Range request for the
|
|
99
|
+
remainder, and verifies the complete blob before publishing it. If the disk
|
|
100
|
+
preflight refuses, free space or point `HF_HUB_CACHE` at another volume and
|
|
101
|
+
rerun. No Python environment and no converted copy inside the mlx-bun checkout
|
|
102
|
+
are involved.
|
|
103
|
+
|
|
104
|
+
The artifact is an external MIT-licensed derivative of
|
|
105
|
+
`zai-org/GLM-5.2-FP8`, converted with Colibri and extended with int8 MTP
|
|
106
|
+
weights. Model weights are not bundled with mlx-bun; review the artifact model
|
|
107
|
+
card before redistribution.
|
|
108
|
+
|
|
62
109
|
## scan + the registry
|
|
63
110
|
|
|
64
111
|
`mlx-bun scan` walks `models--*/snapshots/*` reading only `config.json`
|
|
@@ -50,7 +50,7 @@ Request body (OpenAI chat schema; unknown fields ignored):
|
|
|
50
50
|
"tools": [ /* OpenAI function tools */ ],
|
|
51
51
|
"tool_choice": "auto", // "none" disables tools
|
|
52
52
|
"chat_template_kwargs": { // forwarded to the chat template
|
|
53
|
-
"enable_thinking": false // MiniCPM5 / Qwen3.5:
|
|
53
|
+
"enable_thinking": false // MiniCPM5 / Qwen3.5 / GLM-5.2: think channel
|
|
54
54
|
},
|
|
55
55
|
"reasoning_effort": "medium", // "none"|"minimal"|"low"|"medium"|"high"
|
|
56
56
|
// gates enable_thinking on Qwen3.5/MiniCPM5:
|
|
@@ -78,6 +78,11 @@ Sampling defaults follow the model author's `generation_config.json`
|
|
|
78
78
|
when a field is omitted (optiq serve's gen_config behavior); explicit
|
|
79
79
|
request values always win. MiniCPM5 defaults to the no-think direct
|
|
80
80
|
answer mode unless `chat_template_kwargs.enable_thinking` is `true`.
|
|
81
|
+
GLM-5.2 also defaults to no-think: its generation primer is
|
|
82
|
+
`<think></think>`. Enabling thinking renders `Reasoning Effort: Max` in the
|
|
83
|
+
system turn and leaves an open `<think>` primer for generation. The same
|
|
84
|
+
precedence applies through Chat Completions, Anthropic Messages, and Responses
|
|
85
|
+
because all three normalize into this one chat request path.
|
|
81
86
|
|
|
82
87
|
`stop` sequences are matched on **decoded text**, not token ids, so a
|
|
83
88
|
sequence that spans token boundaries still fires. Generation halts at
|
|
@@ -303,13 +308,16 @@ with `finish_reason: "stop"`. Works on both lanes — serial and
|
|
|
303
308
|
`--batch N` (per-row matchers).
|
|
304
309
|
|
|
305
310
|
Degrade path — a grammar that fails to compile is never rejected (no
|
|
306
|
-
400/500): chat prepends a system
|
|
307
|
-
|
|
308
|
-
`Warning` header (`grammar not enforced: …`);
|
|
309
|
-
chat template to inject into, so it emits the
|
|
310
|
-
`
|
|
311
|
-
|
|
312
|
-
|
|
311
|
+
400/500): chat prepends a string-valued system instruction for the
|
|
312
|
+
requested JSON schema, raw grammar, regex, or choice constraint, and the
|
|
313
|
+
response carries a `Warning` header (`grammar not enforced: …`);
|
|
314
|
+
`/v1/completions` has no chat template to inject into, so it emits the
|
|
315
|
+
`Warning` header only.
|
|
316
|
+
`MLX_BUN_GRAMMAR=0` disables mask compilation but does not silently
|
|
317
|
+
ignore a requested constraint: it uses this same degrade path (chat
|
|
318
|
+
prompt injection plus `Warning`; raw completions `Warning` only).
|
|
319
|
+
Design and fidelity notes:
|
|
320
|
+
[structured-output.md](../design/structured-output.md).
|
|
313
321
|
|
|
314
322
|
### Errors
|
|
315
323
|
|
|
@@ -486,6 +494,23 @@ The served model is FIRST, with extra capability fields:
|
|
|
486
494
|
// lazily loadable)
|
|
487
495
|
"audio": false, // input_audio content parts accepted
|
|
488
496
|
// (tower loaded or lazily loadable)
|
|
497
|
+
"batch_mode": "batch", // loaded cache capability; per-request
|
|
498
|
+
// usage.lane is authoritative
|
|
499
|
+
"tools": true,
|
|
500
|
+
"structured_output": true,
|
|
501
|
+
"embeddings": false,
|
|
502
|
+
"adapters": false,
|
|
503
|
+
"training": false,
|
|
504
|
+
"dsa": true, // GLM-5.2 only; false elsewhere
|
|
505
|
+
"mtp": true, // checkpoint-native GLM MTP is mounted
|
|
506
|
+
"capabilities": { // explicit discoverable surface
|
|
507
|
+
"chat_completions": true, "text_completions": true,
|
|
508
|
+
"anthropic_messages": true, "responses": true,
|
|
509
|
+
"streaming": true, "tools": true, "structured_output": true,
|
|
510
|
+
"logprobs": true, "embeddings": false,
|
|
511
|
+
"vision": false, "audio": false,
|
|
512
|
+
"adapters": false, "training": false
|
|
513
|
+
},
|
|
489
514
|
"gen_defaults": { // model-author sampling defaults
|
|
490
515
|
"temperature": 0.7, // (generation_config.json, with any
|
|
491
516
|
"top_p": 0.95, // --temperature/--top-p/--top-k server
|
|
@@ -518,6 +543,13 @@ matching `mlx_lm.server`.
|
|
|
518
543
|
`input_audio` content parts (tower loaded or lazily loadable — see
|
|
519
544
|
*Audio input* above for what audio requires), so clients can discover
|
|
520
545
|
capabilities instead of probing for a `400`.
|
|
546
|
+
The served-model row also includes `batch_mode: "off" | "serial" | "batch"`:
|
|
547
|
+
the configured cap and the loaded model's cache capability together. Per-turn
|
|
548
|
+
`usage.lane` remains authoritative for a particular request; for example,
|
|
549
|
+
native-MTP requests route serial even when the served GLM model reports
|
|
550
|
+
`batch_mode: "batch"` for ordinary requests.
|
|
551
|
+
For GLM-5.2, the non-generative fields are deliberately false: embeddings,
|
|
552
|
+
vision/audio, adapters, and training are not emulated by the serving port.
|
|
521
553
|
|
|
522
554
|
## GET /health
|
|
523
555
|
|
|
@@ -544,9 +576,40 @@ capabilities instead of probing for a `400`.
|
|
|
544
576
|
"usable_bytes": 0,
|
|
545
577
|
"weights_bytes": 0
|
|
546
578
|
},
|
|
579
|
+
// present only for the direct Colibri GLM-5.2 runtime:
|
|
580
|
+
"glm52": {
|
|
581
|
+
"preset": "g5-32gb-quality",
|
|
582
|
+
"planned_process_bytes": 0,
|
|
583
|
+
"process_limit_bytes": 0,
|
|
584
|
+
"context_tokens": 4096,
|
|
585
|
+
"max_generation_tokens": 128,
|
|
586
|
+
"batch_size": 1,
|
|
587
|
+
"dsa": true,
|
|
588
|
+
"mtp": true,
|
|
589
|
+
"mtp_draft_tokens": 3,
|
|
590
|
+
"resident_weight_bytes": 0,
|
|
591
|
+
"main_expert_slab_bytes": 0,
|
|
592
|
+
"mtp_expert_slab_bytes": 0,
|
|
593
|
+
"expert_runtime": {
|
|
594
|
+
"main_residency": { "resident": 0, "pinned": 0, "working": 0,
|
|
595
|
+
"hits": 0, "misses": 0, "evictions": 0 },
|
|
596
|
+
"mtp_residency": { "resident": 0, "pinned": 0, "working": 0,
|
|
597
|
+
"hits": 0, "misses": 0, "evictions": 0 },
|
|
598
|
+
"last_turn": {
|
|
599
|
+
"main": { "demand": { "hits": 0, "misses": 0,
|
|
600
|
+
"readBytes": 0 } },
|
|
601
|
+
"mtp": { "demand": { "hits": 0, "misses": 0,
|
|
602
|
+
"readBytes": 0 } },
|
|
603
|
+
"pilot": null
|
|
604
|
+
},
|
|
605
|
+
"last_repin": []
|
|
606
|
+
}
|
|
607
|
+
},
|
|
547
608
|
"batch": {
|
|
548
609
|
"configured": 1, // the --batch N value
|
|
549
|
-
"
|
|
610
|
+
"mode": "off", // "off" | "serial" | "batch": configured
|
|
611
|
+
// and model-cache capability together
|
|
612
|
+
"batched": false, // true only when mode == "batch"
|
|
550
613
|
"active_rows": 0, // rows currently decoding in the batch
|
|
551
614
|
"pending_rows": 0, // queued + mid-prefill rows waiting
|
|
552
615
|
"submitted_rows": 0, // cumulative rows routed to the batch lane since start
|
|
@@ -955,6 +1018,15 @@ banner and `mlx-bun fit`. When the eval DB has a real measured decode
|
|
|
955
1018
|
rate for this model snapshot, it is included and takes precedence over
|
|
956
1019
|
the prediction.
|
|
957
1020
|
|
|
1021
|
+
For the direct Colibri GLM-5.2 runtime this endpoint switches to the exact
|
|
1022
|
+
serve-time memory plan. The response separates the full streamed artifact on
|
|
1023
|
+
disk from resident weights, main/MTP expert slabs, KV, and other reserves.
|
|
1024
|
+
`measured_decode_tps` is the G5 warm quality-preserving measurement;
|
|
1025
|
+
`glm52.direct_oracle_warm_decode_tps` is the same-machine direct Colibri
|
|
1026
|
+
control, while `glm52.aspirational_decode_tps` is a target, not a release
|
|
1027
|
+
gate. The single-row SKU result describes the current machine because the
|
|
1028
|
+
generic all-resident SKU estimator is inapplicable.
|
|
1029
|
+
|
|
958
1030
|
```jsonc
|
|
959
1031
|
{
|
|
960
1032
|
"machine": { "chip": "M4 Pro", "ram_bytes": 0, "bandwidth_gbs": 0.0 },
|
|
@@ -973,6 +1045,15 @@ the prediction.
|
|
|
973
1045
|
"max_safe_context": 8192,
|
|
974
1046
|
"predicted_decode_tps": 0.0
|
|
975
1047
|
},
|
|
1048
|
+
// GLM-5.2 only:
|
|
1049
|
+
"glm52": {
|
|
1050
|
+
"artifact_disk_bytes": 0, // exact registry size; null if unavailable
|
|
1051
|
+
"main_expert_slab_bytes": 0,
|
|
1052
|
+
"mtp_expert_slab_bytes": 0,
|
|
1053
|
+
"max_generation_tokens": 128,
|
|
1054
|
+
"direct_oracle_warm_decode_tps": 0.27,
|
|
1055
|
+
"aspirational_decode_tps": 2.0
|
|
1056
|
+
},
|
|
976
1057
|
"sku_matrix_ctx": 32768,
|
|
977
1058
|
"sku_matrix": [{
|
|
978
1059
|
"sku": "M4 Pro 24 GB", "ram_gb": 24,
|
|
@@ -21,7 +21,7 @@ the design rationale is in
|
|
|
21
21
|
| --- | --- | --- | --- |
|
|
22
22
|
| `--host` | addr | `127.0.0.1` | Interface to bind. Loopback-only by default (mlx_lm.server parity); pass `--host 0.0.0.0` to expose the server on your network. |
|
|
23
23
|
| `--port` | n | `8080` | Listen port. A pre-flight probe refuses to start if the port is already serving. |
|
|
24
|
-
| `--memory-budget` | GB | machine RAM × 0.75 | Admission ceiling. Requests whose `prompt + max_tokens` exceed the budget's max safe context are **rejected with 400** (`type: memory_admission`) instead of risking an uncatchable GPU OOM.
|
|
24
|
+
| `--memory-budget` | GB | machine RAM × 0.75; GLM-5.2: min(25 GiB, physical RAM) | Admission ceiling. Requests whose `prompt + max_tokens` exceed the budget's max safe context are **rejected with 400** (`type: memory_admission`) instead of risking an uncatchable GPU OOM. GLM-5.2 runs its header-only exact process equation before opening resident weights or expert slabs, and caps MLX to that plan's allocator reserve. **Decimal GB (×10⁹).** |
|
|
25
25
|
| `--prompt-cache` | GB | `8` | RAM prefix-KV cache (byte-capped LRU; `0` disables). **PREFIX SHARING (2026-07-05): serves are NON-CONSUMING** — a hit hands out zero-copy clones and leaves the donor entry intact, so N agents (or a brand-new session) sharing a system prompt all reuse ONE prefill without destroying each other's conversation entries (the old consume-and-trim take cannibalized the donor). Extended entries supersede their prefix-ancestors (when trimmable), so a conversation stays one entry. Works on BOTH lanes and tiers over the SSD store (Layer 0). |
|
|
26
26
|
| `--isolate` | (bool) | off | Runtime isolation ([runtime-isolation.md](../design/runtime-isolation.md)): the inference engine runs as a CHILD process on a unix socket; this process stays a pure UI/API reverse proxy — instant under any GPU load, survives engine crashes (auto-respawn, in-flight requests get a clean 502). Measured cost ≈0 (−0.4% tok/s, +2 ms TTFT, per-token SSE granularity preserved). `/ws/chat` is not proxied yet (501). `/engine` reports child pid/restarts. |
|
|
27
27
|
| `--model-pool` | n | `1` | With `--isolate`: max RESIDENT model engines. Requests naming another model (**exact `/v1/models` id** — fuzzy strings keep mlx-lm's ignored-field semantics) spawn/route to that model's own engine child (SPAWN-OVERLAP: the new model loads while the old one keeps serving; nobody's stream is interrupted). Over the cap, the LRU engine drains, **demotes its prompt cache to the SSD tier**, and exits — switching back respawns it with state restored from disk. Measured (M1 Max, cpm5⇄qwen0.8b, pool 1): switch 1.5 s, switch-back 1.2 s with `cached_tokens` 103/104 — the conversation survived the eviction. The pool drives this eviction via `POST /admin/drain` — unix-socket-only (never exposed on the public TCP listener) — which quiesces the gateway and demotes the whole prompt cache to the SSD tier before eviction, responding `{ drained: true, demotions: N }`. |
|
|
@@ -38,11 +38,13 @@ the design rationale is in
|
|
|
38
38
|
| `--draft-kind` | `two-model`\|`assistant`\|`dspark`\|`deepspec`\|`ngram` | auto | Override the draft-artifact kind detection (`dspark.json` → dspark; `Gemma4DSparkModel` architecture stamp → deepspec; `*_assistant` config → assistant; else two-model). **`ngram` is never auto-detected — it has no artifact**: MODEL-FREE prompt lookup (drafts copied from the request's own prompt+generation when its trailing k-gram occurred earlier in the stream; port of prompt-lookup decoding / vLLM's `ngram` proposer). Mount it ALONE — `--draft-kind ngram` with a `--draft-model` is refused, as is any other kind without one. Zero weights/memory, lossless by the same verify (gated: serve-loop output token-identical to non-spec greedy on e4b, tests/spec-ngram.test.ts); a no-match round degrades to one plain target step. Best on agentic/RAG/code-edit traffic that re-emits context spans. |
|
|
39
39
|
| `--num-draft-tokens` | n | `3` (`ngram`: `10`) | Drafts per verify round (mlx_lm.server's default; `mlx_lm.generate`'s is 2). A DSpark draft pins this to its trained block width (`cfg.gamma`); ngram drafting is free so its default is the reference implementation's 10. |
|
|
40
40
|
| `--ngram-max` / `--ngram-min` | k | `3` / `1` | `--draft-kind ngram` only: longest/shortest trailing k-gram searched for a match (the reference values — longest first, first occurrence wins). |
|
|
41
|
-
| `--
|
|
41
|
+
| `--mtp` | `on`\|`off` | on for GLM-5.2 | Mount the checkpoint-native GLM MTP row as the server's drafter. It uses the bounded auxiliary expert tier and exact serial verify loop (`usage.lane: "serial+spec"`). `off` removes the draft so ordinary GLM requests can use continuous batching; per-row batched MTP is post-release. Mutually exclusive with an explicit `--draft-model`/`--draft-kind`. |
|
|
42
|
+
| `--context-length` | tokens | `4096` for GLM-5.2 | Context reserved by GLM's header-only resource equation. The same value becomes the request-admission ceiling and is reported in `/stats.glm52`; an impossible plan fails before committing model memory. |
|
|
43
|
+
| `--thinking` | `true`\|`false` | model's own (false for CPM and GLM-5.2) | Server-wide default for the chat template's `enable_thinking` (MiniCPM5/CPM, Qwen3.5, and GLM-5.2). Full precedence: an explicit request `chat_template_kwargs.enable_thinking` always wins; else `reasoning_effort` (`"none"` → off, any other level → on) if the request sent one; else this server default; else the model's own default. GLM renders disabled thinking as `<think></think>` and enabled thinking as an open `<think>` generation primer plus `Reasoning Effort: Max`. |
|
|
42
44
|
| `--temperature` | n ∈ [0,5] | `generation_config.json` | Server-wide sampling default. Per-request `temperature` still wins; the browser chat (sends none) inherits this. `--temp` is accepted as an alias (mlx_lm.server compat); explicit `--temperature` wins if both are given. **Migration note:** mlx_lm.server's `--temp` *default* is `0.0` (unset-temperature requests are greedy there); mlx-bun falls back to the model's `generation_config.json`, then `0.7` — pass `--temp 0` for mlx-lm's behavior. |
|
|
43
45
|
| `--top-p` | n ∈ [0,1] | `generation_config.json` | Server-wide top-p default (per-request `top_p` wins). |
|
|
44
46
|
| `--top-k` | n ∈ [0,1e6] | `generation_config.json` | Server-wide top-k default (per-request `top_k` wins). |
|
|
45
|
-
| `--max-tokens` | n | `65536` chat / `512` raw completion | Completion cap when a request omits `max_tokens` (mlx_lm.server flag; its default there is 512 — `--max-tokens 512` reproduces mlx_lm.server exactly). |
|
|
47
|
+
| `--max-tokens` | n | GLM-5.2: `128`; otherwise `65536` chat / `512` raw completion | Completion cap when a request omits `max_tokens` (mlx_lm.server flag; its default there is 512 — `--max-tokens 512` reproduces mlx_lm.server exactly). For GLM this is also reserved by the pre-open resource equation and must fit inside `--context-length`. |
|
|
46
48
|
| `--allow-private-media` | (bool) | off | Let `image_url`/`audio_url` content parts fetch from **private/loopback/link-local** hosts (a NAS, another LAN box). Off by default: a request's URL is attacker-controlled input, so remote media fetches (`src/media-fetch.ts`) refuse non-http(s) schemes and destinations in private/loopback/link-local/CGNAT ranges — including hosts that *resolve* there and every **redirect hop** (each hop re-passes the whole policy) — which keeps the server from being steered at cloud metadata (`169.254.169.254`) or LAN-internal services (SSRF). Independent of the flag, every remote fetch has a **10 s wall-clock timeout** and a **64 MB response cap** (streaming-enforced, not just Content-Length); violations surface as clean `400`s (`prompt build failed: …`). `data:` URLs decode locally and are never policy-checked. |
|
|
47
49
|
| `--no-open` | (bool) | off | Skip the automatic browser open on start. By default an interactive terminal session opens `http://<host>:<port>/#/chat` once the server is ready; pass this flag to suppress it (e.g. headless or non-TTY environments already skip it). |
|
|
48
50
|
| `--hlg-sampling` | `on`\|`off` | off | Piecewise tone-curve (HLG) sampling: rolls off the top-token region, boosts the mids, gentles the tail. The overall gain folds from `--temperature`. See [docs/design/hlg-sampling.md](../design/hlg-sampling.md). |
|
|
@@ -112,7 +114,8 @@ for paired A/B harnesses).
|
|
|
112
114
|
| `MLX_BUN_PAGED_KV` | `--paged-kv` | off (`==="1"`) | Paged KV cache (see the flag row above — v1: serial, Gemma4, bf16; refusals + prompt-cache bypass apply identically). |
|
|
113
115
|
| `MLX_BUN_ALLOW_PRIVATE_MEDIA` | `--allow-private-media` | off (`==="1"`) | Permit `image_url`/`audio_url` fetches to private/loopback/link-local hosts (the SSRF guard's LAN escape hatch — see the flag row above; timeout + size cap still apply). |
|
|
114
116
|
| `MLX_BUN_PREFILL_TAIL_SPLIT` | — | on (`!=="0"`) | Oracle prefill convention (mlx-lm `generate_step` and its server's batched engine): drain the prompt only to len−1, then compute step-0 logits from a separate **L=1 forward of the last prompt token**. Both lanes. **The SPEC lane (`--draft-model`) follows its own oracle's shape under the same flag** (mlx-lm `speculative_generate_step`, re-anchored 2026-07-07): both target AND draft drain to len−1 and there is **no separate step-0 at all** — the un-drained last prompt token heads the first verify window. Gated live: token-for-token vs the oracle venv incl. a knife-edge cell (tests/spec-serve.test.ts "L1 knife-edge"). (The standalone optiq-oracled assistant loop `specGenerate` deliberately keeps FULL-prompt prefill — optiq's own convention.) `=0` restores the pre-2026-07-07 full-final-chunk convention everywhere (A/B lever + kill switch) — that convention is ulp-different in bf16 at step 0 AND in the last prompt token's stored KV, which flips near-tie greedy streams vs mlx-lm (the 2026-07-07 12B completion-probe divergence). |
|
|
115
|
-
| `
|
|
117
|
+
| `MLX_BUN_GRAMMAR` | — | on (`!=="0"`) | `=0` disables structured-output mask compilation. Requested constraints still take the documented graceful-degrade route: chat prompt injection plus a `Warning` header; raw completions emit the header only. |
|
|
118
|
+
| `MLX_BUN_GRAMMAR_JUMP` | — | **off** (`==="1"`) | Jump-forward decoding for structured output (SGLang's technique via xgrammar's `findJumpForwardString`): when the grammar forces a unique continuation (JSON punctuation/keys; longest under `any_whitespace:false` fixed formatting), the SERIAL lane emits its retokenized ids with ONE multi-token forward instead of one masked forward per token. Lossless in STRING space and always grammar-valid, but the token stream (and, conditioning on it, content after a forced span) can legally differ from an unjumped run — no oracle, hence opt-in. Excluded automatically when either `logprobs` or `top_logprobs` is requested, because jumped tokens have no sampled distribution row; SentencePiece-family tokenizers whose raw encode can't reproduce a mid-stream span simply never jump (guard degrades to normal decode). The batch lane's per-row grammar (`#stepGrammar`) does not jump yet. Tests: tests/grammar-jump.test.ts (contract + Llama-3.2-1B e2e). |
|
|
116
119
|
| `MLX_BUN_SSD_WRITEBEHIND` | — | on (`!=="0"`) | `--ssd-cache`'s debounced write-behind snapshot (restart survival). `=0` disables it entirely — the paired-A/B lever + kill switch for the 2026-07-07 idle-gating fix (eviction/demotion spills still write). The flush is **idle-gated**: every per-tensor step (blocking GPU sync + `writeSync`) waits for the engine to go idle and pauses when a request arrives mid-flush, so durability work never taxes an active decode (pre-fix: a ~16k entry's flush overlapping cached ctx repeats cost e4b ~9% decode@ctx vs mlx-lm). The snapshot timer also never holds batch admission — while rows are active it re-arms instead of grabbing the exclusive. |
|
|
117
120
|
| `MLX_BUN_SSD_SPILL_QUEUE_GB` | — | 2 | Byte cap on the write-behind queue's PENDING spill clones (they pin the evicted entries' GPU memory until the idle-gated flush runs). Over cap, the oldest queued spill drops — its clones free immediately and the entry becomes a future cache miss (never a wrong result). `=0` keeps only the newest + in-flight clone pinned (the soft cap never drops the item just enqueued). `/stats.ssd_cache` reports `pending_spills` / `pending_spill_bytes` / `dropped_spills`. Note: there is deliberately **no shutdown flush** — exiting under traffic loses queued write-behind entries (best-effort tier; cost = one re-prefill). |
|
|
118
121
|
| `MLX_BUN_BATCH_NO_PIPELINE` | — | off (`==="1"`) | Disables the batch lane's pipelined token readback (the A/B lever for the shipped pipelining, like its peers `MLX_BUN_BATCH_EXTEND` / `MLX_BUN_BATCH_SSM` / `MLX_BUN_BATCH_VEC_SAMPLE`). Numerically equivalent, slower steps. |
|
|
@@ -211,7 +214,8 @@ inside it per the table above).
|
|
|
211
214
|
|
|
212
215
|
| Option | serial (`--batch 1`) | `--batch N` (N>1) |
|
|
213
216
|
| --- | --- | --- |
|
|
214
|
-
| `--kv-quant config
|
|
217
|
+
| `--kv-quant config` | ✅ applied to all requests | ✅ batches where the loaded cache capability supports the per-layer scheme; otherwise routes serial |
|
|
218
|
+
| `--kv-quant 4`/`8` | ✅ applied to all requests | ⚠️ uniform-threshold semantics route requests serial |
|
|
215
219
|
| `--kv-quant turbo[:k<bits>v<bits>]` | ✅ applied to all requests | ⚠️ applied, but forces **all** requests to the serial lane (no batching) |
|
|
216
220
|
| `--kv-quant off` | ✅ bf16 | ✅ bf16 (same as the implicit batch default) |
|
|
217
221
|
| *(kv-quant unset)* | bf16 (the L1 default) | **bf16** (Option B) — incl. serial-lane fallback requests |
|
|
@@ -227,6 +231,7 @@ inside it per the table above).
|
|
|
227
231
|
| `tools` / `stop` | ✅ | ✅ (batches) |
|
|
228
232
|
| structured output (`response_format`/`guided_*`) | ✅ (mask in the decode loop) | ✅ (batches; per-row matchers) |
|
|
229
233
|
| `--draft-model` | ✅ spec decode (grammar composes) | ⚠️ mounts, but routes **every** request serial — spec and batching are different modes |
|
|
234
|
+
| GLM `--mtp on` | ✅ native MTP spec decode | ⚠️ default-on MTP routes every request serial+spec; `--mtp off` exposes ordinary GLM batching |
|
|
230
235
|
| `--compiled-decode` | ✅ (serial decode route) | ✅ at **B=1 only** (Phase 3.2): a lone request's adopted serial-class caches replay the same compiled step, same kill switch; B>1 steps run the plain graph |
|
|
231
236
|
| `--fused-sdpa`/`--force-wire` | ✅ (serial decode route) | **n/a — compat mode, no perf flags by design** |
|
|
232
237
|
|
|
@@ -394,9 +399,9 @@ cells in one run).
|
|
|
394
399
|
`--expert-offload`. Inherently the serial recipe until batched
|
|
395
400
|
quantized KV lands.
|
|
396
401
|
|
|
397
|
-
**The two exclusions to remember:** batching
|
|
398
|
-
|
|
399
|
-
cache (v1 bypass; the §7.6 composition is the fix).
|
|
402
|
+
**The two exclusions to remember:** batching excludes uniform/TurboQuant KV
|
|
403
|
+
(per-layer `kv_config.json` does compose with batching), and spec excludes
|
|
404
|
+
prompt-cache reuse (v1 bypass; the §7.6 composition is the fix).
|
|
400
405
|
|
|
401
406
|
## Observability — `GET /stats`
|
|
402
407
|
|
|
@@ -405,9 +410,11 @@ lives in [server-api.md](server-api.md#get-stats) (kept in lockstep with
|
|
|
405
410
|
the code — this section only explains how to read it); highlights:
|
|
406
411
|
`prompt_cache` (8 GB default cap), `kv_quant.mode` (incl. `turbo kXvY`),
|
|
407
412
|
the conditional `ssd_cache` block, and `batch`
|
|
408
|
-
(`active_rows`/`pending_rows`/`submitted_rows`/`kv_bytes
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
413
|
+
(`mode`/`active_rows`/`pending_rows`/`submitted_rows`/`kv_bytes`/
|
|
414
|
+
`kv_budget_bytes`).
|
|
415
|
+
|
|
416
|
+
`batch.mode` is the truthful configured/capability state: `off` for
|
|
417
|
+
`--batch 1`, `serial` when a larger configured cap cannot batch the loaded
|
|
418
|
+
model's cache layout, and `batch` when the model is admitted. `batch.batched`
|
|
419
|
+
is the compatibility boolean for `mode == "batch"`; `active_rows` reports the
|
|
420
|
+
instantaneous live rows.
|
|
@@ -177,7 +177,7 @@ are `DEFAULT_TRAIN_CONFIG` (trainer.ts:89).
|
|
|
177
177
|
| `weight_decay` | float ≥0 | `0.01` | AdamW weight decay (β = `[0.9, 0.999]`, fixed) |
|
|
178
178
|
| `lora_dropout` | float [0,1) | `0.0` | LoRA-input dropout (PEFT-style), training-only regularizer for small sets. Recompute-safe (mask keyed by step+layer, so segmented/checkpointed backward reproduces it) |
|
|
179
179
|
| `rs_lora` | bool | `false` | rsLoRA — scale the update by α/√rank instead of α, so `rank_scaling` changes capacity not step size. Recorded in the adapter config; inference applies the same per-layer scale |
|
|
180
|
-
| `lora_plus_ratio` | float ≥1 | `1.0` | LoRA+ — LR multiplier for the B leaves (A stays at base LR). >1 speeds the B-driven early learning (B is zero-init). 1 = off.
|
|
180
|
+
| `lora_plus_ratio` | float ≥1 | `1.0` | LoRA+ — LR multiplier for the B leaves (A stays at base LR). >1 speeds the B-driven early learning (B is zero-init). 1 = off. The same optimizer policy applies to SFT, DPO, and ORPO |
|
|
181
181
|
| `grad_checkpoint` | bool | `false` | Recompute layer activations in backward (memory↔compute; bit-identical) |
|
|
182
182
|
| `mlp_split` | bool | `false` | Gradient-checkpoint MLP splitting (Gemma4-only, requires `grad_checkpoint`) |
|
|
183
183
|
| `segment_size` | int | `0` (off) | `>0` enables segmented backward — layers per segment (see below) |
|
|
@@ -295,6 +295,15 @@ A finished run writes a PEFT-compatible adapter directory:
|
|
|
295
295
|
- `optiq_lora_config.json` — mlx-bun/optiq adapter metadata (per-layer ranks)
|
|
296
296
|
- `adapter_config.json` — PEFT-compatible config
|
|
297
297
|
|
|
298
|
+
The save completes only after both config files are written, including
|
|
299
|
+
eval-step checkpoints, so a successful process exit always leaves an
|
|
300
|
+
immediately mountable directory. Serving accepts both mlx-lm tensor layout
|
|
301
|
+
(`[in, rank]` / `[rank, out]`) and standard PEFT layout
|
|
302
|
+
(`[rank, in]` / `[out, rank]`, including `base_model.model.*` names).
|
|
303
|
+
Standard PEFT `use_rslora` metadata is honored as
|
|
304
|
+
`lora_alpha / sqrt(rank)`, and saved rsLoRA adapters emit that metadata so
|
|
305
|
+
their training and serving scales agree.
|
|
306
|
+
|
|
298
307
|
When `save_checkpoints: true`, each eval step also writes
|
|
299
308
|
`checkpoints/step-<NNNNN>-val<loss>/` and a durable `metrics.json` (config,
|
|
300
309
|
wall seconds, peak GB, final/best train+val loss, full val trajectory).
|
package/package.json
CHANGED
package/src/anthropic.ts
CHANGED
|
@@ -469,58 +469,68 @@ export function translateOpenAiSse(
|
|
|
469
469
|
const dec = new TextDecoder();
|
|
470
470
|
let buf = "";
|
|
471
471
|
let finalized = false;
|
|
472
|
+
let reader: ReadableStreamDefaultReader<Uint8Array> | null = null;
|
|
473
|
+
let cancelled = false;
|
|
472
474
|
return new ReadableStream<Uint8Array>({
|
|
473
|
-
|
|
475
|
+
start(controller) {
|
|
474
476
|
const emit = (frames: string[]) => {
|
|
475
477
|
for (const f of frames) controller.enqueue(enc.encode(f));
|
|
476
478
|
};
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
if (
|
|
494
|
-
finalized
|
|
495
|
-
|
|
479
|
+
reader = upstream.getReader();
|
|
480
|
+
void (async () => {
|
|
481
|
+
try {
|
|
482
|
+
while (!cancelled) {
|
|
483
|
+
const { done, value } = await reader!.read();
|
|
484
|
+
if (done) break;
|
|
485
|
+
buf += dec.decode(value, { stream: true });
|
|
486
|
+
let sep: number;
|
|
487
|
+
while ((sep = buf.indexOf("\n\n")) !== -1) {
|
|
488
|
+
const frame = buf.slice(0, sep);
|
|
489
|
+
buf = buf.slice(sep + 2);
|
|
490
|
+
const data = frame
|
|
491
|
+
.split("\n")
|
|
492
|
+
.find((l) => l.startsWith("data: "))
|
|
493
|
+
?.slice(6);
|
|
494
|
+
if (data == null) continue;
|
|
495
|
+
if (data === "[DONE]" || data === '"[DONE]"') {
|
|
496
|
+
if (!finalized) {
|
|
497
|
+
finalized = true;
|
|
498
|
+
emit(translator.finalize());
|
|
499
|
+
}
|
|
500
|
+
continue;
|
|
496
501
|
}
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
502
|
+
const parsed = JSON.parse(data);
|
|
503
|
+
if (parsed?.error) {
|
|
504
|
+
emit([
|
|
505
|
+
sse("error", {
|
|
506
|
+
type: "error",
|
|
507
|
+
error: { type: "api_error", message: parsed.error.message ?? "generation failed" },
|
|
508
|
+
}),
|
|
509
|
+
]);
|
|
510
|
+
continue;
|
|
511
|
+
}
|
|
512
|
+
emit(translator.addChunk(parsed));
|
|
508
513
|
}
|
|
509
|
-
emit(translator.addChunk(parsed));
|
|
510
514
|
}
|
|
515
|
+
if (!cancelled && !finalized) emit(translator.finalize());
|
|
516
|
+
} catch (e) {
|
|
517
|
+
if (!cancelled)
|
|
518
|
+
emit([
|
|
519
|
+
sse("error", {
|
|
520
|
+
type: "error",
|
|
521
|
+
error: { type: "api_error", message: (e as Error).message },
|
|
522
|
+
}),
|
|
523
|
+
]);
|
|
524
|
+
} finally {
|
|
525
|
+
reader?.releaseLock();
|
|
526
|
+
reader = null;
|
|
527
|
+
if (!cancelled) controller.close();
|
|
511
528
|
}
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
error: { type: "api_error", message: (e as Error).message },
|
|
518
|
-
}),
|
|
519
|
-
]);
|
|
520
|
-
} finally {
|
|
521
|
-
reader.releaseLock();
|
|
522
|
-
controller.close();
|
|
523
|
-
}
|
|
529
|
+
})();
|
|
530
|
+
},
|
|
531
|
+
async cancel(reason) {
|
|
532
|
+
cancelled = true;
|
|
533
|
+
await reader?.cancel(reason);
|
|
524
534
|
},
|
|
525
535
|
});
|
|
526
536
|
}
|