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
package/src/server.ts
CHANGED
|
@@ -53,9 +53,29 @@ import { Weights } from "./weights";
|
|
|
53
53
|
import { Gemma4Model } from "./model/gemma4";
|
|
54
54
|
import { DiffusionGemmaModel } from "./model/diffusion-gemma";
|
|
55
55
|
import { spliceImageTokens } from "./vision/diffusion-vision";
|
|
56
|
-
import {
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
import {
|
|
57
|
+
createModel,
|
|
58
|
+
openGlm52RuntimeModel,
|
|
59
|
+
type Glm52RuntimeOpenOptions,
|
|
60
|
+
type RuntimeModel,
|
|
61
|
+
} from "./model/factory";
|
|
62
|
+
import { Glm52Model } from "./model/glm52";
|
|
63
|
+
import {
|
|
64
|
+
GLM52_G5_ASPIRATIONAL_DECODE_TPS,
|
|
65
|
+
GLM52_G5_DIRECT_ORACLE_WARM_DECODE_TPS,
|
|
66
|
+
GLM52_G5_MEASURED_AT,
|
|
67
|
+
GLM52_G5_MEASURED_WARM_DECODE_TPS,
|
|
68
|
+
type Glm52MemoryPlan,
|
|
69
|
+
} from "./model/glm52-memory";
|
|
70
|
+
import { isGlm52Config, isMiniCPM5Config } from "./model/support";
|
|
71
|
+
import { Glm52NativeMtpProvider } from "./spec/glm52-mtp-source";
|
|
72
|
+
import {
|
|
73
|
+
generate,
|
|
74
|
+
type GenerateOptions,
|
|
75
|
+
type TokenLogprobs,
|
|
76
|
+
withModelUsageFlush,
|
|
77
|
+
withModelWiredLimit,
|
|
78
|
+
} from "./generate";
|
|
59
79
|
import { cloneKvCaches, SpillQueue } from "./kv-store";
|
|
60
80
|
import { TURBOQUANT_HEAD_DIMS } from "./mlx/turboquant-tables";
|
|
61
81
|
import {
|
|
@@ -245,6 +265,25 @@ export interface ServerContext {
|
|
|
245
265
|
provider: import("./spec/source").DraftProvider;
|
|
246
266
|
numDraftTokens: number;
|
|
247
267
|
} | null;
|
|
268
|
+
/** Present only for the direct Colibri GLM runtime. This is the exact
|
|
269
|
+
* header-derived process equation used before opening resident state. */
|
|
270
|
+
glmMemoryPlan?: Glm52MemoryPlan | null;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export interface LoadContextOptions {
|
|
274
|
+
memoryBudgetBytes?: number;
|
|
275
|
+
/** Direct Colibri runtime/resource overrides. Ignored by other models. */
|
|
276
|
+
glm?: Glm52RuntimeOpenOptions;
|
|
277
|
+
/** Snapshot dir of a draft model for speculative decoding
|
|
278
|
+
* (`--draft-model`). Loaded alongside the target; the pair must share
|
|
279
|
+
* a tokenizer family. */
|
|
280
|
+
draftModelDir?: string;
|
|
281
|
+
/** Drafts per round (`--num-draft-tokens`, mlx_lm.server default 3). */
|
|
282
|
+
numDraftTokens?: number;
|
|
283
|
+
/** Draft-provider kind override (`--draft-kind`). */
|
|
284
|
+
draftKind?: DraftKind;
|
|
285
|
+
ngramMax?: number;
|
|
286
|
+
ngramMin?: number;
|
|
248
287
|
}
|
|
249
288
|
|
|
250
289
|
export interface GenSamplingDefaults {
|
|
@@ -297,33 +336,38 @@ async function loadGenSamplingDefaults(modelDir: string): Promise<GenSamplingDef
|
|
|
297
336
|
|
|
298
337
|
export async function loadContext(
|
|
299
338
|
modelDir: string, modelId?: string,
|
|
300
|
-
opts: {
|
|
301
|
-
memoryBudgetBytes?: number;
|
|
302
|
-
/** Snapshot dir of a draft model for speculative decoding
|
|
303
|
-
* (`--draft-model`). Loaded alongside the target; the pair must share
|
|
304
|
-
* a tokenizer family (hard check below — upstream's silent-garbage
|
|
305
|
-
* mode on mismatched tokenizers isn't worth inheriting). */
|
|
306
|
-
draftModelDir?: string;
|
|
307
|
-
/** Drafts per round (`--num-draft-tokens`, mlx_lm.server default 3). */
|
|
308
|
-
numDraftTokens?: number;
|
|
309
|
-
/** Draft-provider kind override (`--draft-kind`); auto-detected from the
|
|
310
|
-
* draft artifact when absent (dspark.json → dspark; *_assistant config →
|
|
311
|
-
* assistant; otherwise two-model). "ngram" mounts WITHOUT a draft dir. */
|
|
312
|
-
draftKind?: DraftKind;
|
|
313
|
-
/** Prompt-lookup window bounds (`--ngram-max` / `--ngram-min`), ngram
|
|
314
|
-
* kind only. Defaults 3 / 1 (Saxena's reference values). */
|
|
315
|
-
ngramMax?: number;
|
|
316
|
-
ngramMin?: number;
|
|
317
|
-
} = {},
|
|
339
|
+
opts: LoadContextOptions = {},
|
|
318
340
|
): Promise<ServerContext> {
|
|
319
341
|
const config = await loadModelConfig(modelDir);
|
|
320
|
-
const
|
|
342
|
+
const glm = isGlm52Config(config);
|
|
343
|
+
const externalDraft = opts.draftModelDir !== undefined || opts.draftKind !== undefined;
|
|
344
|
+
if (glm && externalDraft && opts.glm?.enableMtp === true)
|
|
345
|
+
throw new Error("native GLM MTP and --draft-model/--draft-kind are mutually exclusive");
|
|
346
|
+
let weights: Weights | null = null;
|
|
347
|
+
let model!: RuntimeModel;
|
|
348
|
+
let glmMemoryPlan: Glm52MemoryPlan | null = null;
|
|
349
|
+
if (glm) {
|
|
350
|
+
const enableNativeMtp = !externalDraft && opts.glm?.enableMtp !== false;
|
|
351
|
+
const opened = await openGlm52RuntimeModel(modelDir, {
|
|
352
|
+
...opts.glm,
|
|
353
|
+
memoryBudgetBytes: opts.memoryBudgetBytes ?? opts.glm?.memoryBudgetBytes,
|
|
354
|
+
// Every drafter owns the serial speculative lane. Do not reserve a
|
|
355
|
+
// simultaneous ordinary batch that cannot be admitted while it is live.
|
|
356
|
+
batchSize: enableNativeMtp || externalDraft ? 1 : opts.glm?.batchSize,
|
|
357
|
+
// An explicit alternate drafter owns the one speculative lane.
|
|
358
|
+
enableMtp: enableNativeMtp,
|
|
359
|
+
});
|
|
360
|
+
model = opened.model;
|
|
361
|
+
glmMemoryPlan = opened.plan;
|
|
362
|
+
} else {
|
|
363
|
+
weights = await Weights.open(modelDir);
|
|
364
|
+
}
|
|
321
365
|
// memoryBudget enforcement at load (Phase 5): Weights.open only mmaps
|
|
322
366
|
// (no GPU allocation yet), so a model whose weights can never serve
|
|
323
367
|
// within the budget is refused HERE — before any unified-memory
|
|
324
368
|
// commitment — with an actionable error instead of a Metal OOM later.
|
|
325
|
-
if (opts.memoryBudgetBytes) {
|
|
326
|
-
const weightsBytes = [...weights
|
|
369
|
+
if (!glm && opts.memoryBudgetBytes) {
|
|
370
|
+
const weightsBytes = [...weights!.shards.files.values()]
|
|
327
371
|
.reduce((a, f) => a + f.mmap.size, 0);
|
|
328
372
|
const report = fit(config, weightsBytes, 1, undefined, undefined, 0, opts.memoryBudgetBytes);
|
|
329
373
|
if (report.maxSafeContext < 1)
|
|
@@ -335,7 +379,7 @@ export async function loadContext(
|
|
|
335
379
|
}
|
|
336
380
|
// generated-specialization dispatch by config fingerprint (Phase C);
|
|
337
381
|
// unmatched configs run the monolith — slow, never broken
|
|
338
|
-
|
|
382
|
+
if (!glm) model = createModel(weights!, config);
|
|
339
383
|
const tokenizer = await loadTokenizer(modelDir);
|
|
340
384
|
// Generation must stop on the tokenizer's eos_token — the chat turn
|
|
341
385
|
// terminator (e.g. Qwen <|im_end|> = 248046). Some configs (Qwen3.5-4B)
|
|
@@ -433,7 +477,9 @@ export async function loadContext(
|
|
|
433
477
|
}
|
|
434
478
|
}
|
|
435
479
|
if (opts.memoryBudgetBytes) {
|
|
436
|
-
const targetBytes =
|
|
480
|
+
const targetBytes = weights
|
|
481
|
+
? [...weights.shards.files.values()].reduce((a, f) => a + f.mmap.size, 0)
|
|
482
|
+
: model.weightsBytes;
|
|
437
483
|
// Draft weights shrink the target's envelope. Draft KV is not modeled
|
|
438
484
|
// (small relative to its weights at serve contexts); admission stays
|
|
439
485
|
// approximately conservative via the combined-weights term.
|
|
@@ -453,9 +499,20 @@ export async function loadContext(
|
|
|
453
499
|
throw new Error(`--draft-kind ${opts.draftKind} requires --draft-model`);
|
|
454
500
|
}
|
|
455
501
|
|
|
502
|
+
// GLM's checkpoint-native MTP row is the production default. It uses the
|
|
503
|
+
// already-planned bounded auxiliary expert tier and the same tokenizer, so
|
|
504
|
+
// there is no second artifact or compatibility probe to load.
|
|
505
|
+
if (!draft && model instanceof Glm52Model && glmMemoryPlan?.enableMtp) {
|
|
506
|
+
draft = {
|
|
507
|
+
provider: new Glm52NativeMtpProvider(model),
|
|
508
|
+
numDraftTokens: glmMemoryPlan.mtpDraftTokens,
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
|
|
456
512
|
return {
|
|
457
513
|
draft,
|
|
458
514
|
model,
|
|
515
|
+
glmMemoryPlan,
|
|
459
516
|
adapters: new AdapterManager(model),
|
|
460
517
|
kvConfig: config.kvQuant,
|
|
461
518
|
genDefaults: await loadGenSamplingDefaults(modelDir),
|
|
@@ -1167,16 +1224,21 @@ export function nextDefaultSeed(): number {
|
|
|
1167
1224
|
* compile fails — the response_format degrades to prompt injection rather
|
|
1168
1225
|
* than a 500, oMLX parity). Returns null for {type:"text"} / unset. */
|
|
1169
1226
|
export function degradeJsonSystemPrompt(body: ChatRequest): string | null {
|
|
1227
|
+
if (body.guided_grammar) {
|
|
1228
|
+
return "You must respond with text matching this grammar:\n\n" + body.guided_grammar;
|
|
1229
|
+
}
|
|
1170
1230
|
const rf = body.response_format as { type?: string; json_schema?: { name?: string; description?: string; schema?: unknown } } | undefined;
|
|
1171
|
-
if (
|
|
1172
|
-
const type = rf.type ?? "text";
|
|
1173
|
-
if (type === "text") return null;
|
|
1174
|
-
if (type === "json_object") {
|
|
1231
|
+
if (rf && typeof rf === "object" && rf.type === "json_object") {
|
|
1175
1232
|
return "You must respond with valid JSON only. " +
|
|
1176
1233
|
"Do not include any explanation or text outside the JSON object.";
|
|
1177
1234
|
}
|
|
1178
|
-
|
|
1179
|
-
|
|
1235
|
+
const schemaSpec = rf && typeof rf === "object" && rf.type === "json_schema"
|
|
1236
|
+
? rf.json_schema
|
|
1237
|
+
: body.structured_outputs && typeof body.structured_outputs === "object"
|
|
1238
|
+
? { schema: body.structured_outputs }
|
|
1239
|
+
: null;
|
|
1240
|
+
if (schemaSpec) {
|
|
1241
|
+
const spec = schemaSpec;
|
|
1180
1242
|
const schema = spec.schema ?? body.structured_outputs ?? {};
|
|
1181
1243
|
const name = spec.name ?? "response";
|
|
1182
1244
|
const description = spec.description ?? "";
|
|
@@ -1186,9 +1248,33 @@ export function degradeJsonSystemPrompt(body: ChatRequest): string | null {
|
|
|
1186
1248
|
"Respond with only the JSON object, no additional text or explanation.";
|
|
1187
1249
|
return prompt;
|
|
1188
1250
|
}
|
|
1251
|
+
if (body.guided_regex) {
|
|
1252
|
+
return "You must respond with text matching this regular expression:\n\n" +
|
|
1253
|
+
body.guided_regex;
|
|
1254
|
+
}
|
|
1255
|
+
if (body.guided_choice?.length) {
|
|
1256
|
+
return "You must respond with exactly one of these choices:\n\n" +
|
|
1257
|
+
body.guided_choice.map((choice) => JSON.stringify(choice)).join("\n");
|
|
1258
|
+
}
|
|
1189
1259
|
return null;
|
|
1190
1260
|
}
|
|
1191
1261
|
|
|
1262
|
+
export function applyGrammarDegrade(
|
|
1263
|
+
body: ChatRequest,
|
|
1264
|
+
degradeHint: string,
|
|
1265
|
+
): { body: ChatRequest; warning: string } {
|
|
1266
|
+
const content = degradeJsonSystemPrompt(body) ??
|
|
1267
|
+
"Follow the requested output constraint exactly.";
|
|
1268
|
+
return {
|
|
1269
|
+
warning:
|
|
1270
|
+
`grammar not enforced: ${degradeHint} - falling back to prompt injection`,
|
|
1271
|
+
body: {
|
|
1272
|
+
...body,
|
|
1273
|
+
messages: [{ role: "system", content }, ...body.messages],
|
|
1274
|
+
},
|
|
1275
|
+
};
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1192
1278
|
export function validateLogprobsParams(body: {
|
|
1193
1279
|
logprobs?: unknown;
|
|
1194
1280
|
top_logprobs?: unknown;
|
|
@@ -1293,9 +1379,14 @@ class LogprobsCollector {
|
|
|
1293
1379
|
*
|
|
1294
1380
|
* Exported for unit tests (serve-detok mlx-lm byte parity). */
|
|
1295
1381
|
export class StreamDecoder {
|
|
1382
|
+
static readonly #WINDOW_TOKENS = 32;
|
|
1296
1383
|
#ids: number[] = [];
|
|
1297
1384
|
#emitted = "";
|
|
1298
1385
|
#warnedRevision = false;
|
|
1386
|
+
/** Token index where the bounded decode suffix begins. */
|
|
1387
|
+
#windowStart = 0;
|
|
1388
|
+
/** Exact decoded text before #windowStart, established by suffix matching. */
|
|
1389
|
+
#windowPrefix = "";
|
|
1299
1390
|
readonly #trimLeadingSpace: boolean;
|
|
1300
1391
|
readonly #bareSpaceId: number | undefined;
|
|
1301
1392
|
|
|
@@ -1308,17 +1399,50 @@ export class StreamDecoder {
|
|
|
1308
1399
|
}
|
|
1309
1400
|
|
|
1310
1401
|
#decode(ids: number[]): string {
|
|
1311
|
-
const full = this
|
|
1402
|
+
const full = this.#decodeRaw(ids);
|
|
1312
1403
|
return this.#trimLeadingSpace && full.startsWith(" ") ? full.slice(1) : full;
|
|
1313
1404
|
}
|
|
1314
1405
|
|
|
1406
|
+
#decodeRaw(ids: number[]): string {
|
|
1407
|
+
return this.tokenizer.decode(ids, this.skipSpecialTokens);
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
/** Decode only a bounded suffix once an exact text anchor can be proven.
|
|
1411
|
+
*
|
|
1412
|
+
* At each rebase, decode a candidate suffix independently and accept it only
|
|
1413
|
+
* when it is literally the suffix of the exact text decoded so far. This
|
|
1414
|
+
* preserves ByteLevel/SPM boundary behavior while reducing steady-state
|
|
1415
|
+
* decode work from the whole generation to 32–64 tokens. If no suffix can be
|
|
1416
|
+
* anchored (an unusual/global decoder), leave #windowStart at zero and keep
|
|
1417
|
+
* the full-history correctness path. */
|
|
1418
|
+
#decodeIncremental(): string {
|
|
1419
|
+
const full = this.#windowStart === 0
|
|
1420
|
+
? this.#decode(this.#ids)
|
|
1421
|
+
: this.#windowPrefix + this.#decodeRaw(this.#ids.slice(this.#windowStart));
|
|
1422
|
+
|
|
1423
|
+
if (this.#ids.length - this.#windowStart < StreamDecoder.#WINDOW_TOKENS * 2)
|
|
1424
|
+
return full;
|
|
1425
|
+
|
|
1426
|
+
const preferred = this.#ids.length - StreamDecoder.#WINDOW_TOKENS;
|
|
1427
|
+
const oldest = Math.max(this.#windowStart, preferred - StreamDecoder.#WINDOW_TOKENS);
|
|
1428
|
+
for (let start = preferred; start >= oldest; start--) {
|
|
1429
|
+
const tail = this.#decodeRaw(this.#ids.slice(start));
|
|
1430
|
+
if ((tail.length > 0 || full.length === 0) && full.endsWith(tail)) {
|
|
1431
|
+
this.#windowStart = start;
|
|
1432
|
+
this.#windowPrefix = full.slice(0, full.length - tail.length);
|
|
1433
|
+
break;
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
return full;
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1315
1439
|
push(token: number): string {
|
|
1316
1440
|
this.#ids.push(token);
|
|
1317
1441
|
// Bare-space hold-back (mlx-lm add_token keeps "Ġ" in _unflushed): don't
|
|
1318
1442
|
// advance #emitted; the held space(s) flush as part of the next
|
|
1319
1443
|
// non-bare-space token's delta — consecutive bare spaces accumulate.
|
|
1320
1444
|
if (token === this.#bareSpaceId) return "";
|
|
1321
|
-
const full = this.#
|
|
1445
|
+
const full = this.#decodeIncremental();
|
|
1322
1446
|
// hold back a trailing replacement char (partial multi-byte sequence)
|
|
1323
1447
|
const stable = full.endsWith("�") ? full.slice(0, -1) : full;
|
|
1324
1448
|
if (!stable.startsWith(this.#emitted)) {
|
|
@@ -1396,6 +1520,8 @@ export function createServer(
|
|
|
1396
1520
|
// agentic sub-agent workload. --batch 1 pins strict serial for
|
|
1397
1521
|
// arrival-independent numerics. 8 = optiq's Mac-safe concurrency.
|
|
1398
1522
|
const batch = Math.max(1, Math.floor(serverOptions.batch ?? 8));
|
|
1523
|
+
const defaultGeneratedTokens =
|
|
1524
|
+
serverOptions.defaultMaxTokens ?? ctx.glmMemoryPlan?.maxGenerationTokens;
|
|
1399
1525
|
|
|
1400
1526
|
// KV-quant scheme, resolved once. UNSET now means bf16 (flipped
|
|
1401
1527
|
// 2026-07-05 with the naked-=-L1 default): quantized KV measured 5–20%
|
|
@@ -1634,9 +1760,15 @@ export function createServer(
|
|
|
1634
1760
|
!options.pagedKv
|
|
1635
1761
|
) {
|
|
1636
1762
|
const { specServeRun } = await import("./spec/serve-loop");
|
|
1637
|
-
return
|
|
1638
|
-
ctx.model,
|
|
1639
|
-
|
|
1763
|
+
return withModelWiredLimit(
|
|
1764
|
+
ctx.model,
|
|
1765
|
+
() => withModelUsageFlush(
|
|
1766
|
+
ctx.model,
|
|
1767
|
+
() => specServeRun(
|
|
1768
|
+
ctx.model, ctx.draft!.provider, ctx.draft!.numDraftTokens,
|
|
1769
|
+
promptIds, options, onToken,
|
|
1770
|
+
),
|
|
1771
|
+
),
|
|
1640
1772
|
);
|
|
1641
1773
|
}
|
|
1642
1774
|
// Cache entries are adapter-specific: KV computed under one adapter
|
|
@@ -1880,17 +2012,27 @@ export function createServer(
|
|
|
1880
2012
|
// fit() solves max safe context from weights + KV growth + prefill
|
|
1881
2013
|
// transient; the KV term assumes bf16 (a kv-quant scheme stretches the
|
|
1882
2014
|
// real ceiling, never shrinks it — admission stays conservative).
|
|
1883
|
-
const
|
|
2015
|
+
const genericAdmission = fit(
|
|
1884
2016
|
ctx.model.config, ctx.model.weightsBytes, 1,
|
|
1885
2017
|
undefined, undefined, 0, serverOptions.memoryBudgetBytes,
|
|
1886
2018
|
);
|
|
2019
|
+
const admission = ctx.glmMemoryPlan
|
|
2020
|
+
? {
|
|
2021
|
+
...genericAdmission,
|
|
2022
|
+
maxSafeContext: ctx.glmMemoryPlan.contextTokens,
|
|
2023
|
+
usableBytes: ctx.glmMemoryPlan.processLimitBytes,
|
|
2024
|
+
}
|
|
2025
|
+
: genericAdmission;
|
|
1887
2026
|
if (admission.maxSafeContext < 1)
|
|
1888
2027
|
throw new Error(
|
|
1889
2028
|
`memory budget ${(admission.usableBytes / 1e9).toFixed(2)} GB cannot serve ` +
|
|
1890
2029
|
`${ctx.modelId} (weights ${(ctx.model.weightsBytes / 1e9).toFixed(2)} GB): ` +
|
|
1891
2030
|
`no context fits — raise the budget or pick a smaller model`,
|
|
1892
2031
|
);
|
|
1893
|
-
if (
|
|
2032
|
+
if (ctx.glmMemoryPlan)
|
|
2033
|
+
setMemoryLimit(ctx.glmMemoryPlan.lineItems.allocatorReserveBytes);
|
|
2034
|
+
else if (serverOptions.memoryBudgetBytes)
|
|
2035
|
+
setMemoryLimit(serverOptions.memoryBudgetBytes);
|
|
1894
2036
|
|
|
1895
2037
|
// /library response cache (30 s) — registry + config reads only.
|
|
1896
2038
|
let libraryCache: { at: number; rows: unknown[] } | null = null;
|
|
@@ -1964,7 +2106,8 @@ export function createServer(
|
|
|
1964
2106
|
// truncates the visible reply. Default very generously (the model's
|
|
1965
2107
|
// context is far larger); only an explicit max_tokens narrows it. The
|
|
1966
2108
|
// model still stops at its eos_token well before this in normal replies.
|
|
1967
|
-
maxTokens: req.max_completion_tokens ?? req.max_tokens ??
|
|
2109
|
+
maxTokens: req.max_completion_tokens ?? req.max_tokens ??
|
|
2110
|
+
defaultGeneratedTokens ?? 65_536,
|
|
1968
2111
|
temperature: req.temperature ?? serverOptions.defaultTemperature ?? defaultTemp,
|
|
1969
2112
|
topP: req.top_p ?? serverOptions.defaultTopP ?? ctx.genDefaults.topP ?? 0,
|
|
1970
2113
|
topK: req.top_k ?? serverOptions.defaultTopK ?? ctx.genDefaults.topK ?? 0,
|
|
@@ -2004,7 +2147,12 @@ export function createServer(
|
|
|
2004
2147
|
const compileGrammarForRequest = async (
|
|
2005
2148
|
req: ChatRequest,
|
|
2006
2149
|
): Promise<{ controller: import("./grammar").GrammarController | null; degradeHint: string | null }> => {
|
|
2007
|
-
if (!grammarEnabled())
|
|
2150
|
+
if (!grammarEnabled()) {
|
|
2151
|
+
return {
|
|
2152
|
+
controller: null,
|
|
2153
|
+
degradeHint: "grammar compilation disabled by MLX_BUN_GRAMMAR=0",
|
|
2154
|
+
};
|
|
2155
|
+
}
|
|
2008
2156
|
const r = await compileGrammarRequest(
|
|
2009
2157
|
req as GrammarRequest,
|
|
2010
2158
|
ctx.tokenizer,
|
|
@@ -2383,16 +2531,65 @@ export function createServer(
|
|
|
2383
2531
|
const machine = thisMachine();
|
|
2384
2532
|
const chip = detectChip();
|
|
2385
2533
|
let expertsBytes = 0;
|
|
2534
|
+
let artifactDiskBytes: number | null = null;
|
|
2386
2535
|
let measured: { decodeTps: number; ts: number } | null = null;
|
|
2387
2536
|
try {
|
|
2388
2537
|
const { Registry } = await import("./registry");
|
|
2389
2538
|
const rec = new Registry().list().find((r) => r.repoId === ctx.modelId);
|
|
2390
2539
|
if (rec) {
|
|
2391
2540
|
expertsBytes = rec.expertsBytes;
|
|
2541
|
+
artifactDiskBytes = rec.sizeBytes;
|
|
2392
2542
|
const { EvalDB } = await import("./evaldb");
|
|
2393
2543
|
measured = new EvalDB().latestFor(rec.path);
|
|
2394
2544
|
}
|
|
2395
2545
|
} catch {}
|
|
2546
|
+
if (ctx.glmMemoryPlan) {
|
|
2547
|
+
const plan = ctx.glmMemoryPlan;
|
|
2548
|
+
const li = plan.lineItems;
|
|
2549
|
+
const kvBytes = li.targetKvBytes + li.mtpKvBytes;
|
|
2550
|
+
const transientBytes = plan.plannedProcessBytes -
|
|
2551
|
+
li.residentWeightsBytes - li.mainExpertSlabBytes -
|
|
2552
|
+
li.mtpExpertSlabBytes - kvBytes;
|
|
2553
|
+
return Response.json({
|
|
2554
|
+
machine: {
|
|
2555
|
+
chip: chip.name,
|
|
2556
|
+
ram_bytes: machine.ramBytes,
|
|
2557
|
+
bandwidth_gbs: machine.bandwidthGBs,
|
|
2558
|
+
},
|
|
2559
|
+
context_tokens: plan.contextTokens,
|
|
2560
|
+
typical_context_tokens: plan.contextTokens,
|
|
2561
|
+
typical_decode_tps: GLM52_G5_MEASURED_WARM_DECODE_TPS,
|
|
2562
|
+
measured_decode_tps: GLM52_G5_MEASURED_WARM_DECODE_TPS,
|
|
2563
|
+
measured_at: GLM52_G5_MEASURED_AT,
|
|
2564
|
+
report: {
|
|
2565
|
+
fits: true,
|
|
2566
|
+
weights_bytes: li.residentWeightsBytes,
|
|
2567
|
+
kv_bytes: kvBytes,
|
|
2568
|
+
transient_bytes: transientBytes,
|
|
2569
|
+
total_bytes: plan.plannedProcessBytes,
|
|
2570
|
+
usable_bytes: plan.processLimitBytes,
|
|
2571
|
+
max_safe_context: plan.contextTokens,
|
|
2572
|
+
predicted_decode_tps: null,
|
|
2573
|
+
},
|
|
2574
|
+
glm52: {
|
|
2575
|
+
artifact_disk_bytes: artifactDiskBytes,
|
|
2576
|
+
main_expert_slab_bytes: li.mainExpertSlabBytes,
|
|
2577
|
+
mtp_expert_slab_bytes: li.mtpExpertSlabBytes,
|
|
2578
|
+
max_generation_tokens: plan.maxGenerationTokens,
|
|
2579
|
+
direct_oracle_warm_decode_tps:
|
|
2580
|
+
GLM52_G5_DIRECT_ORACLE_WARM_DECODE_TPS,
|
|
2581
|
+
aspirational_decode_tps: GLM52_G5_ASPIRATIONAL_DECODE_TPS,
|
|
2582
|
+
},
|
|
2583
|
+
sku_matrix_ctx: plan.contextTokens,
|
|
2584
|
+
sku_matrix: [{
|
|
2585
|
+
sku: chip.name,
|
|
2586
|
+
ram_gb: Math.round(machine.ramBytes / 2 ** 30),
|
|
2587
|
+
fits: true,
|
|
2588
|
+
max_context: plan.contextTokens,
|
|
2589
|
+
decode_tps: GLM52_G5_MEASURED_WARM_DECODE_TPS,
|
|
2590
|
+
}],
|
|
2591
|
+
});
|
|
2592
|
+
}
|
|
2396
2593
|
const report = fit(
|
|
2397
2594
|
ctx.model.config, ctx.model.weightsBytes, admission.maxSafeContext,
|
|
2398
2595
|
machine, undefined, expertsBytes, serverOptions.memoryBudgetBytes,
|
|
@@ -2513,14 +2710,44 @@ export function createServer(
|
|
|
2513
2710
|
},
|
|
2514
2711
|
admission: {
|
|
2515
2712
|
max_safe_context: admission.maxSafeContext,
|
|
2516
|
-
memory_budget_bytes:
|
|
2713
|
+
memory_budget_bytes:
|
|
2714
|
+
ctx.glmMemoryPlan?.processLimitBytes ??
|
|
2715
|
+
serverOptions.memoryBudgetBytes ?? null,
|
|
2517
2716
|
usable_bytes: admission.usableBytes,
|
|
2518
2717
|
weights_bytes: ctx.model.weightsBytes,
|
|
2519
2718
|
},
|
|
2719
|
+
...(ctx.glmMemoryPlan ? {
|
|
2720
|
+
glm52: {
|
|
2721
|
+
preset: ctx.glmMemoryPlan.preset,
|
|
2722
|
+
planned_process_bytes: ctx.glmMemoryPlan.plannedProcessBytes,
|
|
2723
|
+
process_limit_bytes: ctx.glmMemoryPlan.processLimitBytes,
|
|
2724
|
+
context_tokens: ctx.glmMemoryPlan.contextTokens,
|
|
2725
|
+
max_generation_tokens: ctx.glmMemoryPlan.maxGenerationTokens,
|
|
2726
|
+
batch_size: ctx.glmMemoryPlan.batchSize,
|
|
2727
|
+
dsa: ctx.model instanceof Glm52Model && ctx.model.capabilities.dsa,
|
|
2728
|
+
mtp: ctx.draft?.provider.id === "glm52-native-mtp",
|
|
2729
|
+
mtp_draft_tokens: ctx.glmMemoryPlan.mtpDraftTokens,
|
|
2730
|
+
resident_weight_bytes: ctx.glmMemoryPlan.lineItems.residentWeightsBytes,
|
|
2731
|
+
main_expert_slab_bytes: ctx.glmMemoryPlan.lineItems.mainExpertSlabBytes,
|
|
2732
|
+
mtp_expert_slab_bytes: ctx.glmMemoryPlan.lineItems.mtpExpertSlabBytes,
|
|
2733
|
+
expert_runtime: ctx.model instanceof Glm52Model &&
|
|
2734
|
+
ctx.model.expertRuntime
|
|
2735
|
+
? {
|
|
2736
|
+
main_residency:
|
|
2737
|
+
ctx.model.expertRuntime.manager.snapshot(),
|
|
2738
|
+
mtp_residency:
|
|
2739
|
+
ctx.model.expertRuntime.mtp?.manager.snapshot() ?? null,
|
|
2740
|
+
last_turn: ctx.model.expertRuntime.lastTelemetry,
|
|
2741
|
+
last_repin: ctx.model.expertRuntime.lastRepin,
|
|
2742
|
+
}
|
|
2743
|
+
: null,
|
|
2744
|
+
},
|
|
2745
|
+
} : {}),
|
|
2520
2746
|
// --batch: configured cap, whether batching is live for this model,
|
|
2521
2747
|
// and rows currently decoding in the batch.
|
|
2522
2748
|
batch: {
|
|
2523
2749
|
configured: batch,
|
|
2750
|
+
mode: gateway.batchMode,
|
|
2524
2751
|
batched: gateway.batchingEnabled,
|
|
2525
2752
|
active_rows: gateway.activeRows,
|
|
2526
2753
|
pending_rows: gateway.pendingRows,
|
|
@@ -2578,7 +2805,9 @@ export function createServer(
|
|
|
2578
2805
|
};
|
|
2579
2806
|
const data: Array<Record<string, unknown>> = [{
|
|
2580
2807
|
id: ctx.modelId, object: "model", created, owned_by: "mlx-bun",
|
|
2581
|
-
context_window:
|
|
2808
|
+
context_window:
|
|
2809
|
+
ctx.glmMemoryPlan?.contextTokens ??
|
|
2810
|
+
ctx.model.config.text.maxPositionEmbeddings,
|
|
2582
2811
|
// Capability flags for clients (CLI/external pi) that build a
|
|
2583
2812
|
// provider from discovery — `reasoning` gates the thinking toggle,
|
|
2584
2813
|
// `vision` the image input declaration, `audio` whether
|
|
@@ -2587,6 +2816,29 @@ export function createServer(
|
|
|
2587
2816
|
reasoning: ctx.template.supportsThinking,
|
|
2588
2817
|
vision: !!(ctx.vision || ctx.loadVision),
|
|
2589
2818
|
audio: !!(ctx.audio || ctx.loadAudio),
|
|
2819
|
+
batch_mode: gateway.batchMode,
|
|
2820
|
+
tools: true,
|
|
2821
|
+
structured_output: true,
|
|
2822
|
+
embeddings: isEmbeddingModel(ctx.model),
|
|
2823
|
+
adapters: !(ctx.model instanceof Glm52Model),
|
|
2824
|
+
training: !(ctx.model instanceof Glm52Model),
|
|
2825
|
+
dsa: ctx.model instanceof Glm52Model && ctx.model.capabilities.dsa,
|
|
2826
|
+
mtp: ctx.draft?.provider.id === "glm52-native-mtp",
|
|
2827
|
+
capabilities: {
|
|
2828
|
+
chat_completions: true,
|
|
2829
|
+
text_completions: true,
|
|
2830
|
+
anthropic_messages: true,
|
|
2831
|
+
responses: true,
|
|
2832
|
+
streaming: true,
|
|
2833
|
+
tools: true,
|
|
2834
|
+
structured_output: true,
|
|
2835
|
+
logprobs: true,
|
|
2836
|
+
embeddings: isEmbeddingModel(ctx.model),
|
|
2837
|
+
vision: !!(ctx.vision || ctx.loadVision),
|
|
2838
|
+
audio: !!(ctx.audio || ctx.loadAudio),
|
|
2839
|
+
adapters: !(ctx.model instanceof Glm52Model),
|
|
2840
|
+
training: !(ctx.model instanceof Glm52Model),
|
|
2841
|
+
},
|
|
2590
2842
|
gen_defaults: genDefaultsOut,
|
|
2591
2843
|
}];
|
|
2592
2844
|
try {
|
|
@@ -2812,7 +3064,7 @@ export function createServer(
|
|
|
2812
3064
|
// translates its body into this shape and the Response back —
|
|
2813
3065
|
// generation, tools, vision, stop sequences, prompt cache, and
|
|
2814
3066
|
// admission control all live here exactly once.
|
|
2815
|
-
const handleChat = async (body: ChatRequest): Promise<Response> => {
|
|
3067
|
+
const handleChat = async (body: ChatRequest, signal: AbortSignal): Promise<Response> => {
|
|
2816
3068
|
if (!Array.isArray(body.messages) || body.messages.length === 0)
|
|
2817
3069
|
return Response.json({ error: { message: "messages required" } }, { status: 400 });
|
|
2818
3070
|
// mlx-lm validates logprobs params up front (ValueError → 400)
|
|
@@ -2859,14 +3111,9 @@ export function createServer(
|
|
|
2859
3111
|
const g = await compileGrammarForRequest(body);
|
|
2860
3112
|
grammarCtrl = g.controller;
|
|
2861
3113
|
if (!g.controller && g.degradeHint) {
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
messages: [
|
|
2866
|
-
{ role: "system", content: degradeJsonSystemPrompt(body) },
|
|
2867
|
-
...body.messages,
|
|
2868
|
-
],
|
|
2869
|
-
};
|
|
3114
|
+
const degraded = applyGrammarDegrade(body, g.degradeHint);
|
|
3115
|
+
grammarWarning = degraded.warning;
|
|
3116
|
+
body = degraded.body;
|
|
2870
3117
|
}
|
|
2871
3118
|
}
|
|
2872
3119
|
// Free what a rejected request has already allocated (grammar WASM
|
|
@@ -3113,16 +3360,22 @@ export function createServer(
|
|
|
3113
3360
|
recordLane(id, lane);
|
|
3114
3361
|
|
|
3115
3362
|
if (body.stream) {
|
|
3363
|
+
const streamAbort = new AbortController();
|
|
3364
|
+
const generationSignal = AbortSignal.any([signal, streamAbort.signal]);
|
|
3365
|
+
let cancelled = false;
|
|
3116
3366
|
const stream = new ReadableStream<Uint8Array>({
|
|
3117
|
-
|
|
3367
|
+
start(controller) {
|
|
3118
3368
|
const enc = new TextEncoder();
|
|
3119
|
-
const send = (obj: unknown) =>
|
|
3120
|
-
|
|
3369
|
+
const send = (obj: unknown) => {
|
|
3370
|
+
if (!generationSignal.aborted)
|
|
3371
|
+
controller.enqueue(enc.encode(`data: ${JSON.stringify(obj)}\n\n`));
|
|
3372
|
+
};
|
|
3121
3373
|
const chunk = (delta: Record<string, unknown>, finish: string | null) => ({
|
|
3122
3374
|
id, object: "chat.completion.chunk", created, model: ctx.modelId,
|
|
3123
3375
|
choices: [{ index: 0, delta, finish_reason: finish }],
|
|
3124
3376
|
});
|
|
3125
|
-
|
|
3377
|
+
void (async () => {
|
|
3378
|
+
try {
|
|
3126
3379
|
// The gateway owns lane selection + GPU exclusivity; this body
|
|
3127
3380
|
// runs per-request (concurrently in batched mode, each writing
|
|
3128
3381
|
// its own SSE stream — the per-row fan-out).
|
|
@@ -3157,7 +3410,8 @@ export function createServer(
|
|
|
3157
3410
|
return new Promise<void>((r) => setImmediate(r));
|
|
3158
3411
|
}
|
|
3159
3412
|
}
|
|
3160
|
-
}, vision, shape);
|
|
3413
|
+
}, vision, shape, generationSignal);
|
|
3414
|
+
if (generationSignal.aborted) return;
|
|
3161
3415
|
// a stop match discards everything from the match on,
|
|
3162
3416
|
// including text still held by the decoders
|
|
3163
3417
|
let tail = "";
|
|
@@ -3206,12 +3460,21 @@ export function createServer(
|
|
|
3206
3460
|
});
|
|
3207
3461
|
// bare sentinel per the OpenAI spec — JSON.stringify would
|
|
3208
3462
|
// quote it and strict SDK clients never see the terminator
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3463
|
+
controller.enqueue(enc.encode("data: [DONE]\n\n"));
|
|
3464
|
+
} catch (e) {
|
|
3465
|
+
if (!generationSignal.aborted)
|
|
3466
|
+
send({ error: { message: (e as Error).message } });
|
|
3467
|
+
} finally {
|
|
3468
|
+
if (!cancelled) {
|
|
3469
|
+
if (generationSignal.aborted) controller.error(generationSignal.reason);
|
|
3470
|
+
else controller.close();
|
|
3471
|
+
}
|
|
3472
|
+
}
|
|
3473
|
+
})();
|
|
3474
|
+
},
|
|
3475
|
+
cancel(reason) {
|
|
3476
|
+
cancelled = true;
|
|
3477
|
+
streamAbort.abort(reason);
|
|
3215
3478
|
},
|
|
3216
3479
|
});
|
|
3217
3480
|
return new Response(stream, {
|
|
@@ -3244,7 +3507,7 @@ export function createServer(
|
|
|
3244
3507
|
content += parts.content;
|
|
3245
3508
|
reasoning += parts.reasoning;
|
|
3246
3509
|
if (stopper.stopped) return false; // halt generation
|
|
3247
|
-
}, vision, shape);
|
|
3510
|
+
}, vision, shape, signal);
|
|
3248
3511
|
if (!stopper.stopped) {
|
|
3249
3512
|
const flushed = router.flush();
|
|
3250
3513
|
reasoning += router.takeReasoning();
|
|
@@ -3304,7 +3567,7 @@ export function createServer(
|
|
|
3304
3567
|
} catch {
|
|
3305
3568
|
return Response.json({ error: { message: "invalid JSON body" } }, { status: 400 });
|
|
3306
3569
|
}
|
|
3307
|
-
return handleChat(body);
|
|
3570
|
+
return handleChat(body, request.signal);
|
|
3308
3571
|
}
|
|
3309
3572
|
|
|
3310
3573
|
// Raw text completion (mlx_lm.server's /v1/completions, request_type
|
|
@@ -3358,7 +3621,8 @@ export function createServer(
|
|
|
3358
3621
|
const g = await compileGrammarForRequest(body as unknown as ChatRequest);
|
|
3359
3622
|
if (g.controller) options.grammar = g.controller;
|
|
3360
3623
|
else if (g.degradeHint)
|
|
3361
|
-
textGrammarWarning =
|
|
3624
|
+
textGrammarWarning =
|
|
3625
|
+
`grammar not enforced: ${g.degradeHint} - no prompt injection on /v1/completions`;
|
|
3362
3626
|
}
|
|
3363
3627
|
// Mirrors the chat lane: a real controller (not the degrade path)
|
|
3364
3628
|
// shapes the request for per-row grammar batching.
|
|
@@ -3366,7 +3630,8 @@ export function createServer(
|
|
|
3366
3630
|
// mlx_lm.server's default max_tokens is 512 (its --max-tokens CLI
|
|
3367
3631
|
// default). The chat lane's very generous default is wrong for raw
|
|
3368
3632
|
// completion: with no template an EOS may never come.
|
|
3369
|
-
options.maxTokens = body.max_completion_tokens ?? body.max_tokens ??
|
|
3633
|
+
options.maxTokens = body.max_completion_tokens ?? body.max_tokens ??
|
|
3634
|
+
defaultGeneratedTokens ?? 512;
|
|
3370
3635
|
const requiredCtx = promptIds.length + options.maxTokens;
|
|
3371
3636
|
if (requiredCtx > admission.maxSafeContext) {
|
|
3372
3637
|
// Pre-run reject: the gateway only takes grammar disposal ownership
|
|
@@ -3427,16 +3692,22 @@ export function createServer(
|
|
|
3427
3692
|
stopped ? "stop" : generated >= (options.maxTokens ?? 512) ? "length" : "stop";
|
|
3428
3693
|
|
|
3429
3694
|
if (body.stream) {
|
|
3695
|
+
const streamAbort = new AbortController();
|
|
3696
|
+
const generationSignal = AbortSignal.any([request.signal, streamAbort.signal]);
|
|
3697
|
+
let cancelled = false;
|
|
3430
3698
|
const stream = new ReadableStream<Uint8Array>({
|
|
3431
|
-
|
|
3699
|
+
start(controller) {
|
|
3432
3700
|
const enc = new TextEncoder();
|
|
3433
|
-
const send = (obj: unknown) =>
|
|
3434
|
-
|
|
3701
|
+
const send = (obj: unknown) => {
|
|
3702
|
+
if (!generationSignal.aborted)
|
|
3703
|
+
controller.enqueue(enc.encode(`data: ${JSON.stringify(obj)}\n\n`));
|
|
3704
|
+
};
|
|
3435
3705
|
const chunk = (text: string, finish: string | null) => ({
|
|
3436
3706
|
id, object: "text_completion", created, model: ctx.modelId,
|
|
3437
3707
|
choices: [{ index: 0, text, finish_reason: finish }],
|
|
3438
3708
|
});
|
|
3439
|
-
|
|
3709
|
+
void (async () => {
|
|
3710
|
+
try {
|
|
3440
3711
|
const decoder = new StreamDecoder(ctx.tokenizer);
|
|
3441
3712
|
const stopper = new StopMatcher(options.stopSequences);
|
|
3442
3713
|
// ≥25 ms macrotask hop, same reason as the chat lane: keep the
|
|
@@ -3453,7 +3724,8 @@ export function createServer(
|
|
|
3453
3724
|
return new Promise<void>((r) => setImmediate(r));
|
|
3454
3725
|
}
|
|
3455
3726
|
}
|
|
3456
|
-
}, undefined, shape);
|
|
3727
|
+
}, undefined, shape, generationSignal);
|
|
3728
|
+
if (generationSignal.aborted) return;
|
|
3457
3729
|
if (!stopper.stopped) {
|
|
3458
3730
|
let tail = stopper.push(decoder.flush());
|
|
3459
3731
|
if (!stopper.stopped) tail += stopper.flush();
|
|
@@ -3472,12 +3744,21 @@ export function createServer(
|
|
|
3472
3744
|
...(s.spec ? { speculation: s.spec } : {}),
|
|
3473
3745
|
},
|
|
3474
3746
|
});
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3747
|
+
controller.enqueue(enc.encode("data: [DONE]\n\n"));
|
|
3748
|
+
} catch (e) {
|
|
3749
|
+
if (!generationSignal.aborted)
|
|
3750
|
+
send({ error: { message: (e as Error).message } });
|
|
3751
|
+
} finally {
|
|
3752
|
+
if (!cancelled) {
|
|
3753
|
+
if (generationSignal.aborted) controller.error(generationSignal.reason);
|
|
3754
|
+
else controller.close();
|
|
3755
|
+
}
|
|
3756
|
+
}
|
|
3757
|
+
})();
|
|
3758
|
+
},
|
|
3759
|
+
cancel(reason) {
|
|
3760
|
+
cancelled = true;
|
|
3761
|
+
streamAbort.abort(reason);
|
|
3481
3762
|
},
|
|
3482
3763
|
});
|
|
3483
3764
|
return new Response(stream, {
|
|
@@ -3501,7 +3782,7 @@ export function createServer(
|
|
|
3501
3782
|
lpc?.push(token, lpInfo);
|
|
3502
3783
|
text += stopper.push(decoder.push(token));
|
|
3503
3784
|
if (stopper.stopped) return false; // halt generation
|
|
3504
|
-
}, undefined, shape);
|
|
3785
|
+
}, undefined, shape, request.signal);
|
|
3505
3786
|
if (!stopper.stopped) {
|
|
3506
3787
|
let tail = stopper.push(decoder.flush());
|
|
3507
3788
|
if (!stopper.stopped) tail += stopper.flush();
|
|
@@ -3548,7 +3829,7 @@ export function createServer(
|
|
|
3548
3829
|
} catch (e) {
|
|
3549
3830
|
return anthropicError(400, "invalid_request_error", (e as Error).message);
|
|
3550
3831
|
}
|
|
3551
|
-
const resp = await handleChat(chatBody);
|
|
3832
|
+
const resp = await handleChat(chatBody, request.signal);
|
|
3552
3833
|
if (!resp.ok) {
|
|
3553
3834
|
const err = (await resp.json().catch(() => null)) as
|
|
3554
3835
|
| { error?: { message?: string } }
|
|
@@ -3629,7 +3910,7 @@ export function createServer(
|
|
|
3629
3910
|
} catch (e) {
|
|
3630
3911
|
return responsesError(400, (e as Error).message);
|
|
3631
3912
|
}
|
|
3632
|
-
const resp = await handleChat(chatBody);
|
|
3913
|
+
const resp = await handleChat(chatBody, request.signal);
|
|
3633
3914
|
if (!resp.ok) {
|
|
3634
3915
|
const err = (await resp.json().catch(() => null)) as
|
|
3635
3916
|
| { error?: { message?: string } }
|