bleam 0.0.9 → 0.0.11

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 (68) hide show
  1. package/dist/ai.cjs +608 -59
  2. package/dist/ai.d.cts +217 -89
  3. package/dist/ai.d.ts +217 -89
  4. package/dist/ai.js +601 -59
  5. package/dist/cli.cjs +142 -94
  6. package/dist/cli.d.cts +11 -27
  7. package/dist/cli.d.ts +11 -27
  8. package/dist/cli.js +143 -95
  9. package/dist/files.d.cts +1 -1
  10. package/dist/files.d.ts +1 -1
  11. package/dist/platform.cjs +1 -13
  12. package/dist/platform.d.cts +1 -26
  13. package/dist/platform.d.ts +1 -26
  14. package/dist/platform.js +1 -10
  15. package/dist/schema.d.cts +1 -1
  16. package/dist/schema.d.ts +1 -1
  17. package/dist/state.d.cts +1 -1
  18. package/dist/state.d.ts +1 -1
  19. package/dist/{ui-Bg11tvlc.d.ts → ui-CHc4xEs_.d.ts} +7 -7
  20. package/dist/ui.d.ts +1 -1
  21. package/dist/window.d.ts +1 -1
  22. package/package.json +2 -1
  23. package/templates/image-generation/app/index.tsx +3 -1
  24. package/templates/native/ios/Bleam/AI/Flux2/Configuration/ModelRegistry.swift +1 -1
  25. package/templates/native/ios/Bleam/AI/Flux2/Pipeline/FluxImageRunner.swift +7 -2
  26. package/templates/native/ios/Bleam/AI/TextGeneration/Bonsai/BonsaiTextGenerator.swift +46 -0
  27. package/templates/native/ios/Bleam/AI/TextGeneration/Bonsai/BonsaiTextModel.swift +107 -0
  28. package/templates/native/ios/Bleam/AI/TextGeneration/Bonsai/BonsaiTextSnapshotValidator.swift +201 -0
  29. package/templates/native/ios/Bleam/AI/TextGeneration/Core/GenerationTypes.swift +85 -0
  30. package/templates/native/ios/Bleam/AI/TextGeneration/Core/ModelCapabilities.swift +21 -0
  31. package/templates/native/ios/Bleam/AI/TextGeneration/Core/TextKVCache.swift +23 -0
  32. package/templates/native/ios/Bleam/AI/TextGeneration/Core/TokenSampler.swift +79 -0
  33. package/templates/native/ios/Bleam/AI/TextGeneration/Qwen3/Qwen3CheckpointLoader.swift +161 -0
  34. package/templates/native/ios/Bleam/AI/TextGeneration/Qwen3/Qwen3Configuration.swift +119 -0
  35. package/templates/native/ios/Bleam/AI/TextGeneration/Qwen3/Qwen3InferenceSession.swift +84 -0
  36. package/templates/native/ios/Bleam/AI/TextGeneration/Qwen3/Qwen3Layers.swift +197 -0
  37. package/templates/native/ios/Bleam/AI/TextGeneration/Qwen3/Qwen3Model.swift +130 -0
  38. package/templates/native/ios/Bleam/AI/TextGeneration/Qwen35/Qwen35Checkpoint.swift +102 -0
  39. package/templates/native/ios/Bleam/AI/TextGeneration/Qwen35/Qwen35Configuration.swift +140 -0
  40. package/templates/native/ios/Bleam/AI/TextGeneration/Qwen35/Qwen35Model.swift +112 -0
  41. package/templates/native/ios/Bleam/AI/TextGeneration/Qwen35/Qwen35Session.swift +121 -0
  42. package/templates/native/ios/Bleam/AI/TextGeneration/QwenHybrid/QwenHybridCache.swift +42 -0
  43. package/templates/native/ios/Bleam/AI/TextGeneration/QwenHybrid/QwenHybridFullAttention.swift +57 -0
  44. package/templates/native/ios/Bleam/AI/TextGeneration/QwenHybrid/QwenHybridLayers.swift +58 -0
  45. package/templates/native/ios/Bleam/AI/TextGeneration/QwenHybrid/QwenHybridLinearAttention.swift +111 -0
  46. package/templates/native/ios/Bleam/AppDelegate.swift +0 -97
  47. package/templates/native/ios/Bleam.xcodeproj/project.pbxproj +128 -121
  48. package/templates/native/ios/GenerationService/BonsaiTextGenerationRunner.swift +103 -0
  49. package/templates/native/ios/GenerationService/ImageGenerationRunner.swift +19 -7
  50. package/templates/native/ios/GenerationService/main.swift +174 -48
  51. package/templates/native/ios/Shared/Generation/GenerationServiceProtocol.swift +3 -0
  52. package/templates/native/ios/Shared/Generation/GenerationWorkerProtocol.swift +44 -0
  53. package/templates/native/modules/bleam-runtime/ios/AIModule.swift +225 -5
  54. package/templates/native/modules/bleam-runtime/ios/GenerationContracts.swift +47 -0
  55. package/templates/native/modules/bleam-runtime/ios/PlatformModule.swift +0 -175
  56. package/templates/text-generation/app/index.tsx +93 -0
  57. package/templates/text-generation/app.config.ts +5 -0
  58. package/templates/updates/README.md +116 -0
  59. package/templates/updates/src/index.ts +292 -0
  60. package/templates/updates/src/schema.ts +230 -0
  61. package/templates/updates/tsconfig.json +12 -0
  62. package/templates/updates/wrangler.jsonc +19 -0
  63. package/templates/native/ios/PlatformHelper/Info.plist +0 -29
  64. package/templates/native/ios/PlatformHelper/main.swift +0 -335
  65. /package/dist/{files-DwA7pzr3.d.cts → files-4ZEoAWiv.d.ts} +0 -0
  66. /package/dist/{files-VrkQlKIT.d.ts → files-Dt5mbzLq.d.cts} +0 -0
  67. /package/dist/{schema-DOOjfXvs.d.ts → schema-CYh6n8GS.d.ts} +0 -0
  68. /package/dist/{schema-ENSMX_1t.d.cts → schema-oeOrd3i1.d.cts} +0 -0
package/dist/ai.cjs CHANGED
@@ -11,6 +11,53 @@ react = require_chunk.__toESM(react);
11
11
  let react_native = require("react-native");
12
12
  react_native = require_chunk.__toESM(react_native);
13
13
 
14
+ //#region src/ai/bonsai.ts
15
+ const internalTextModelCatalog = [
16
+ {
17
+ id: "ternary-bonsai-1.7b",
18
+ repo: "prism-ml/Ternary-Bonsai-1.7B-mlx-2bit",
19
+ revision: "5f3e306330f636cfc6c6241b4850fae6711c5985",
20
+ estimatedDownloadBytes: 495528947,
21
+ contextLength: 32768,
22
+ architecture: "qwen3"
23
+ },
24
+ {
25
+ id: "ternary-bonsai-4b",
26
+ repo: "prism-ml/Ternary-Bonsai-4B-mlx-2bit",
27
+ revision: "e1374ad6bf9b1b56afd743936b8faa33c409a75f",
28
+ estimatedDownloadBytes: 1143060044,
29
+ contextLength: 32768,
30
+ architecture: "qwen3"
31
+ },
32
+ {
33
+ id: "ternary-bonsai-8b",
34
+ repo: "prism-ml/Ternary-Bonsai-8B-mlx-2bit",
35
+ revision: "9260b24298e4211e804663e9f519962cf59f34be",
36
+ estimatedDownloadBytes: 2315155948,
37
+ contextLength: 65536,
38
+ architecture: "qwen3"
39
+ },
40
+ {
41
+ id: "ternary-bonsai-27b",
42
+ repo: "prism-ml/Ternary-Bonsai-27B-mlx-2bit",
43
+ revision: "70f75f3ad081ab840a42f3304c02c27e7f89bfb7",
44
+ estimatedDownloadBytes: 8510972679,
45
+ contextLength: 262144,
46
+ architecture: "qwen3.5"
47
+ }
48
+ ];
49
+ const bonsaiTextModelCatalog = Object.freeze(internalTextModelCatalog.map(({ architecture: _,...metadata }) => Object.freeze(metadata)));
50
+ function bonsai(id) {
51
+ const text = internalTextModelCatalog.some((model) => model.id === id);
52
+ if (!text && id !== "ternary-bonsai-image-4b") throw new TypeError(`Unsupported Bonsai model: ${id}`);
53
+ return Object.freeze({
54
+ provider: "bonsai",
55
+ kind: text ? "text" : "image",
56
+ id
57
+ });
58
+ }
59
+
60
+ //#endregion
14
61
  //#region src/ai/errors.ts
15
62
  var AIError = class extends Error {
16
63
  constructor(code, message, cause) {
@@ -50,6 +97,217 @@ function mapNativeError(error) {
50
97
  return new AIError("generation_failed", message, error);
51
98
  }
52
99
 
100
+ //#endregion
101
+ //#region src/ai/bonsai-text.ts
102
+ const bonsaiTextModelFiles = [
103
+ "config.json",
104
+ "tokenizer.json",
105
+ "tokenizer_config.json",
106
+ "chat_template.jinja",
107
+ "model.safetensors",
108
+ "model.safetensors.index.json"
109
+ ];
110
+ function modelId(model) {
111
+ return typeof model === "string" ? model : model.id;
112
+ }
113
+ function metadataFor(model) {
114
+ const id = modelId(model);
115
+ const metadata = bonsaiTextModelCatalog.find((candidate) => candidate.id === id);
116
+ if (!metadata) throw new AIError("invalid_model", `Unsupported Bonsai text model: ${id}`);
117
+ return metadata;
118
+ }
119
+ function bonsaiTextModelDirectory(model) {
120
+ return require_files.files.directory(require_files.files.paths.models, "bonsai", "text", modelId(model));
121
+ }
122
+ function modelDirectory(model, override) {
123
+ return override ? require_files.files.directory(override) : bonsaiTextModelDirectory(model);
124
+ }
125
+ function modelDirectoryIn(model, directory) {
126
+ return directory ? require_files.files.directory(directory, modelId(model)) : bonsaiTextModelDirectory(model);
127
+ }
128
+ function resolveUrl(repo, revision, file) {
129
+ return `https://huggingface.co/${repo.split("/").map(encodeURIComponent).join("/")}/resolve/${encodeURIComponent(revision)}/${encodeURIComponent(file)}`;
130
+ }
131
+ function throwIfAborted$1(signal) {
132
+ if (signal?.aborted) throw new AIError("generation_canceled", "Bonsai model preparation canceled");
133
+ }
134
+ const fileSizes = {
135
+ "ternary-bonsai-1.7b": {
136
+ "config.json": 2939,
137
+ "tokenizer.json": 11422650,
138
+ "tokenizer_config.json": 348,
139
+ "chat_template.jinja": 4063,
140
+ "model.safetensors": 484049216,
141
+ "model.safetensors.index.json": 49731
142
+ },
143
+ "ternary-bonsai-4b": {
144
+ "config.json": 3115,
145
+ "tokenizer.json": 11422650,
146
+ "tokenizer_config.json": 348,
147
+ "chat_template.jinja": 4063,
148
+ "model.safetensors": 1131565944,
149
+ "model.safetensors.index.json": 63924
150
+ },
151
+ "ternary-bonsai-8b": {
152
+ "config.json": 3118,
153
+ "tokenizer.json": 11422650,
154
+ "tokenizer_config.json": 348,
155
+ "chat_template.jinja": 4063,
156
+ "model.safetensors": 2303661704,
157
+ "model.safetensors.index.json": 64065
158
+ },
159
+ "ternary-bonsai-27b": {
160
+ "config.json": 3790,
161
+ "tokenizer.json": 19989343,
162
+ "tokenizer_config.json": 1098,
163
+ "chat_template.jinja": 7764,
164
+ "model.safetensors": 8490785104,
165
+ "model.safetensors.index.json": 185580
166
+ }
167
+ };
168
+ function validFile(model, file, size) {
169
+ return size === fileSizes[model][file];
170
+ }
171
+ async function stateFor(model, rootOverride) {
172
+ const metadata = metadataFor(model);
173
+ const root = modelDirectory(model, rootOverride);
174
+ const missingFiles = [];
175
+ const invalidFiles = [];
176
+ let bytesPresent = 0;
177
+ for (const file of bonsaiTextModelFiles) {
178
+ const stat = await root.file(file).stat();
179
+ if (stat.type !== "file") missingFiles.push(file);
180
+ else {
181
+ bytesPresent += stat.size ?? 0;
182
+ if (!validFile(metadata.id, file, stat.size)) invalidFiles.push(file);
183
+ }
184
+ }
185
+ const expectedRevision = metadata.revision;
186
+ const manifestFile = root.file("manifest.json");
187
+ if ((await manifestFile.stat()).type !== "file") missingFiles.push("manifest.json");
188
+ else try {
189
+ const manifest = await manifestFile.json();
190
+ if (!isRecord(manifest) || manifest.formatVersion !== 1 || manifest.model !== metadata.id || manifest.revision !== expectedRevision) invalidFiles.push("manifest.json");
191
+ } catch {
192
+ invalidFiles.push("manifest.json");
193
+ }
194
+ if (!missingFiles.includes("config.json")) try {
195
+ const config = await root.file("config.json").json();
196
+ if (!validConfig(metadata.id, config)) invalidFiles.push("config.json");
197
+ } catch {
198
+ invalidFiles.push("config.json");
199
+ }
200
+ const installed = missingFiles.length === 0;
201
+ return {
202
+ model: bonsai(metadata.id),
203
+ metadata,
204
+ path: root.path,
205
+ installed,
206
+ verified: installed && invalidFiles.length === 0,
207
+ bytesPresent,
208
+ estimatedTotalBytes: metadata.estimatedDownloadBytes,
209
+ missingFiles,
210
+ invalidFiles
211
+ };
212
+ }
213
+ function validConfig(model, value) {
214
+ if (!isRecord(value)) return false;
215
+ const expected = {
216
+ "ternary-bonsai-1.7b": [
217
+ 2048,
218
+ 28,
219
+ 32768
220
+ ],
221
+ "ternary-bonsai-4b": [
222
+ 2560,
223
+ 36,
224
+ 32768
225
+ ],
226
+ "ternary-bonsai-8b": [
227
+ 4096,
228
+ 36,
229
+ 65536
230
+ ],
231
+ "ternary-bonsai-27b": [
232
+ 5120,
233
+ 64,
234
+ 262144
235
+ ]
236
+ }[model];
237
+ const config = model === "ternary-bonsai-27b" ? value.text_config : value;
238
+ if (!isRecord(config)) return false;
239
+ const quantization = config.quantization;
240
+ return isRecord(quantization) && quantization.bits === 2 && quantization.group_size === 128 && config.hidden_size === expected[0] && config.num_hidden_layers === expected[1] && config.max_position_embeddings === expected[2];
241
+ }
242
+ async function prepareBonsaiTextModel(model, options = {}) {
243
+ const metadata = metadataFor(model);
244
+ const root = modelDirectoryIn(model, options.directory);
245
+ throwIfAborted$1(options.signal);
246
+ await root.create({ recursive: true });
247
+ const headers = options.accessToken ? { Authorization: `Bearer ${options.accessToken}` } : void 0;
248
+ const completedBytes = /* @__PURE__ */ new Map();
249
+ const emit = (file, index, progress) => {
250
+ completedBytes.set(file, progress.bytesWritten);
251
+ const modelBytesWritten = Array.from(completedBytes.values()).reduce((total, bytes) => total + bytes, 0);
252
+ options.onProgress?.({
253
+ model: metadata.id,
254
+ file,
255
+ index,
256
+ total: bonsaiTextModelFiles.length,
257
+ ...progress,
258
+ modelBytesWritten,
259
+ estimatedModelBytes: metadata.estimatedDownloadBytes,
260
+ modelFraction: Math.min(1, modelBytesWritten / metadata.estimatedDownloadBytes)
261
+ });
262
+ };
263
+ for (const [fileIndex, file] of bonsaiTextModelFiles.entries()) {
264
+ throwIfAborted$1(options.signal);
265
+ const index = fileIndex + 1;
266
+ const destination = root.file(file);
267
+ const expectedBytes = fileSizes[metadata.id][file];
268
+ const stat = await destination.stat();
269
+ if (stat.type === "file" && validFile(metadata.id, file, stat.size)) {
270
+ emit(file, index, {
271
+ bytesWritten: stat.size ?? 0,
272
+ totalBytes: stat.size,
273
+ fraction: 1
274
+ });
275
+ continue;
276
+ }
277
+ await destination.download(resolveUrl(metadata.repo, metadata.revision, file), {
278
+ headers,
279
+ expectedBytes,
280
+ atomic: true,
281
+ resume: true,
282
+ signal: options.signal,
283
+ onProgress(progress) {
284
+ emit(file, index, progress);
285
+ }
286
+ }).result;
287
+ throwIfAborted$1(options.signal);
288
+ }
289
+ await root.file("manifest.json").write(JSON.stringify({
290
+ formatVersion: 1,
291
+ model: metadata.id,
292
+ revision: metadata.revision
293
+ }));
294
+ const state = await stateFor(metadata.id, root.path);
295
+ if (!state.verified) throw new AIError("invalid_model", `Downloaded Bonsai model is incomplete: ${[...state.missingFiles, ...state.invalidFiles].join(", ")}`);
296
+ return {
297
+ model: state.model,
298
+ path: root.path
299
+ };
300
+ }
301
+ function verifyBonsaiTextModel(model, options = {}) {
302
+ return stateFor(model, options.path);
303
+ }
304
+ function listBonsaiTextModels(options = {}) {
305
+ return Promise.all(bonsaiTextModelCatalog.map((metadata) => stateFor(metadata.id, options.directory ? require_files.files.directory(options.directory, metadata.id).path : void 0)));
306
+ }
307
+ async function deleteBonsaiTextModel(model, options = {}) {
308
+ await modelDirectory(model, options.path).delete({ recursive: true });
309
+ }
310
+
53
311
  //#endregion
54
312
  //#region src/ai/image-batch.ts
55
313
  function batchImageSeeds(count, seed) {
@@ -247,7 +505,51 @@ const flux2Klein4B8BitAbliterated = {
247
505
  }
248
506
  }
249
507
  };
250
- const imageModelManifests = [flux2Klein4B8BitAbliterated];
508
+ const ternaryBonsaiImage4B = {
509
+ formatVersion: 1,
510
+ model: "ternary-bonsai-image-4b",
511
+ directory: "ternary-bonsai-image-4b",
512
+ metadata: {
513
+ displayName: "Ternary Bonsai Image 4B",
514
+ family: "bonsai-image",
515
+ precision: "2-bit",
516
+ estimatedMemoryGB: 8,
517
+ quality: "high",
518
+ speed: "fast",
519
+ capabilities: ["text-to-image"]
520
+ },
521
+ components: {
522
+ textEncoder: {
523
+ repo: "prism-ml/bonsai-image-ternary-4B-mlx-2bit",
524
+ revision: "2c24c81b934a658ba5590cf39088ba929985b4a8",
525
+ path: "text-encoder",
526
+ files: [
527
+ "text_encoder-mlx-4bit/config.json",
528
+ "text_encoder-mlx-4bit/tokenizer.json",
529
+ "text_encoder-mlx-4bit/tokenizer_config.json",
530
+ "text_encoder-mlx-4bit/model.safetensors",
531
+ "text_encoder-mlx-4bit/model.safetensors.index.json"
532
+ ]
533
+ },
534
+ transformer: {
535
+ repo: "prism-ml/bonsai-image-ternary-4B-mlx-2bit",
536
+ revision: "2c24c81b934a658ba5590cf39088ba929985b4a8",
537
+ path: "transformer",
538
+ files: [
539
+ "transformer-packed-mflux/config.json",
540
+ "transformer-packed-mflux/diffusion_pytorch_model.safetensors",
541
+ "transformer-packed-mflux/quantization_config.json"
542
+ ]
543
+ },
544
+ vae: {
545
+ repo: "prism-ml/bonsai-image-ternary-4B-mlx-2bit",
546
+ revision: "2c24c81b934a658ba5590cf39088ba929985b4a8",
547
+ path: "vae",
548
+ files: ["vae/config.json", "vae/diffusion_pytorch_model.safetensors"]
549
+ }
550
+ }
551
+ };
552
+ const imageModelManifests = [flux2Klein4B8BitAbliterated, ternaryBonsaiImage4B];
251
553
  function imageModelManifest(model) {
252
554
  return imageModelManifests.find((manifest) => manifest.model === model);
253
555
  }
@@ -317,15 +619,90 @@ const flux2KleinModelFiles = [
317
619
  }
318
620
  ];
319
621
  const flux2KleinTotalBytes = flux2KleinModelFiles.reduce((total, file) => total + file.bytes, 0);
622
+ const bonsaiRepo = ternaryBonsaiImage4B.components.textEncoder.repo;
623
+ const bonsaiRevision = ternaryBonsaiImage4B.components.textEncoder.revision;
624
+ const ternaryBonsaiImageModelFiles = [
625
+ {
626
+ repo: bonsaiRepo,
627
+ revision: bonsaiRevision,
628
+ source: "text_encoder-mlx-4bit/config.json",
629
+ destination: "text-encoder/config.json",
630
+ bytes: 937
631
+ },
632
+ {
633
+ repo: bonsaiRepo,
634
+ revision: bonsaiRevision,
635
+ source: "text_encoder-mlx-4bit/tokenizer.json",
636
+ destination: "text-encoder/tokenizer.json",
637
+ bytes: 11422654
638
+ },
639
+ {
640
+ repo: bonsaiRepo,
641
+ revision: bonsaiRevision,
642
+ source: "text_encoder-mlx-4bit/tokenizer_config.json",
643
+ destination: "text-encoder/tokenizer_config.json",
644
+ bytes: 9706
645
+ },
646
+ {
647
+ repo: bonsaiRepo,
648
+ revision: bonsaiRevision,
649
+ source: "text_encoder-mlx-4bit/model.safetensors",
650
+ destination: "text-encoder/model.safetensors",
651
+ bytes: 2263022529
652
+ },
653
+ {
654
+ repo: bonsaiRepo,
655
+ revision: bonsaiRevision,
656
+ source: "text_encoder-mlx-4bit/model.safetensors.index.json",
657
+ destination: "text-encoder/model.safetensors.index.json",
658
+ bytes: 63924
659
+ },
660
+ {
661
+ repo: bonsaiRepo,
662
+ revision: bonsaiRevision,
663
+ source: "transformer-packed-mflux/config.json",
664
+ destination: "transformer/config.json",
665
+ bytes: 619
666
+ },
667
+ {
668
+ repo: bonsaiRepo,
669
+ revision: bonsaiRevision,
670
+ source: "transformer-packed-mflux/diffusion_pytorch_model.safetensors",
671
+ destination: "transformer/diffusion_pytorch_model.safetensors",
672
+ bytes: 1425271472
673
+ },
674
+ {
675
+ repo: bonsaiRepo,
676
+ revision: bonsaiRevision,
677
+ source: "transformer-packed-mflux/quantization_config.json",
678
+ destination: "transformer/quantization_config.json",
679
+ bytes: 5054
680
+ },
681
+ {
682
+ repo: bonsaiRepo,
683
+ revision: bonsaiRevision,
684
+ source: "vae/config.json",
685
+ destination: "vae/config.json",
686
+ bytes: 821
687
+ },
688
+ {
689
+ repo: bonsaiRepo,
690
+ revision: bonsaiRevision,
691
+ source: "vae/diffusion_pytorch_model.safetensors",
692
+ destination: "vae/diffusion_pytorch_model.safetensors",
693
+ bytes: 168120878
694
+ }
695
+ ];
696
+ const ternaryBonsaiImageTotalBytes = ternaryBonsaiImageModelFiles.reduce((total, file) => total + file.bytes, 0);
320
697
  function joinModelPath(root, component) {
321
698
  return `${root.replace(/\/+$/, "")}/${component.replace(/^\/+/, "")}`;
322
699
  }
323
- function fluxImageModelPaths(modelRoot, manifest = flux2Klein4B8BitAbliterated) {
700
+ function fluxImageModelPaths(path, manifest = flux2Klein4B8BitAbliterated) {
324
701
  return {
325
- modelRoot,
326
- textEncoderPath: joinModelPath(modelRoot, manifest.components.textEncoder.path),
327
- transformerPath: joinModelPath(modelRoot, manifest.components.transformer.path),
328
- vaePath: joinModelPath(modelRoot, manifest.components.vae.path)
702
+ path,
703
+ textEncoderPath: joinModelPath(path, manifest.components.textEncoder.path),
704
+ transformerPath: joinModelPath(path, manifest.components.transformer.path),
705
+ vaePath: joinModelPath(path, manifest.components.vae.path)
329
706
  };
330
707
  }
331
708
 
@@ -2424,14 +2801,31 @@ var Tokenizer_default = Tokenizer;
2424
2801
 
2425
2802
  //#endregion
2426
2803
  //#region src/ai/image.ts
2427
- function modelCacheRoot(manifest = flux2Klein4B8BitAbliterated) {
2804
+ function imageModelId(model) {
2805
+ return typeof model === "string" ? model : model.id;
2806
+ }
2807
+ function imageModelDefinition(model) {
2808
+ const id = imageModelId(model);
2809
+ if (id === flux2Klein4B8BitAbliterated.model) return {
2810
+ manifest: flux2Klein4B8BitAbliterated,
2811
+ files: flux2KleinModelFiles,
2812
+ totalBytes: flux2KleinTotalBytes
2813
+ };
2814
+ if (id === ternaryBonsaiImage4B.model) return {
2815
+ manifest: ternaryBonsaiImage4B,
2816
+ files: ternaryBonsaiImageModelFiles,
2817
+ totalBytes: ternaryBonsaiImageTotalBytes
2818
+ };
2819
+ throw new AIError("invalid_model", `Unsupported image model: ${id}`);
2820
+ }
2821
+ function modelCacheRoot(manifest) {
2428
2822
  return require_files.files.directory(require_files.files.paths.models, manifest.directory);
2429
2823
  }
2430
2824
  function assertImageModel(model) {
2431
- if (model !== flux2Klein4B8BitAbliterated.model) throw new AIError("invalid_model", `Unsupported image model: ${model}`);
2825
+ imageModelDefinition(model);
2432
2826
  }
2433
- function modelRootFromOptions(modelRoot) {
2434
- return modelRoot ?? modelCacheRoot().path;
2827
+ function modelPathFromOptions(definition, path) {
2828
+ return path ?? modelCacheRoot(definition.manifest).path;
2435
2829
  }
2436
2830
  function downloadConcurrency(value) {
2437
2831
  if (value === void 0) return 2;
@@ -2598,11 +2992,11 @@ function cachedLoRAFile(lora) {
2598
2992
  return imageLoRARoot().directory(id).file(safeCacheName(sourceFileName(lora), "lora filename"));
2599
2993
  }
2600
2994
  async function ensureModelFile(input) {
2601
- const destination = require_files.files.file(input.modelRoot, input.file.destination);
2995
+ const destination = require_files.files.file(input.path, input.file.destination);
2602
2996
  const stat = await destination.stat();
2603
2997
  if (stat.type === "file" && stat.size === input.file.bytes) {
2604
2998
  input.onProgress?.({
2605
- model: flux2Klein4B8BitAbliterated.model,
2999
+ model: input.model,
2606
3000
  file: input.file.destination,
2607
3001
  index: input.index,
2608
3002
  total: input.total,
@@ -2619,7 +3013,7 @@ async function ensureModelFile(input) {
2619
3013
  resume: true,
2620
3014
  onProgress(progress) {
2621
3015
  input.onProgress?.({
2622
- model: flux2Klein4B8BitAbliterated.model,
3016
+ model: input.model,
2623
3017
  file: input.file.destination,
2624
3018
  index: input.index,
2625
3019
  total: input.total,
@@ -2628,16 +3022,16 @@ async function ensureModelFile(input) {
2628
3022
  }
2629
3023
  }).result;
2630
3024
  }
2631
- async function writePreparedManifest(modelRoot) {
2632
- await require_files.files.file(modelRoot, "manifest.json").write(JSON.stringify(flux2Klein4B8BitAbliterated, null, 2));
3025
+ async function writePreparedManifest(path, manifest) {
3026
+ await require_files.files.file(path, "manifest.json").write(JSON.stringify(manifest, null, 2));
2633
3027
  }
2634
- async function imageModelState(model, modelRoot) {
2635
- assertImageModel(model);
3028
+ async function imageModelState(model, path) {
3029
+ const definition = imageModelDefinition(model);
2636
3030
  let bytesPresent = 0;
2637
3031
  const missingFiles = [];
2638
3032
  const invalidFiles = [];
2639
- for (const file of flux2KleinModelFiles) {
2640
- const stat = await require_files.files.file(modelRoot, file.destination).stat();
3033
+ for (const file of definition.files) {
3034
+ const stat = await require_files.files.file(path, file.destination).stat();
2641
3035
  if (stat.type !== "file") {
2642
3036
  missingFiles.push(file.destination);
2643
3037
  continue;
@@ -2645,17 +3039,17 @@ async function imageModelState(model, modelRoot) {
2645
3039
  bytesPresent += Math.min(stat.size ?? 0, file.bytes);
2646
3040
  if (stat.size !== file.bytes) invalidFiles.push(file.destination);
2647
3041
  }
2648
- const installed = missingFiles.length === 0;
2649
- const manifestStat = await require_files.files.file(modelRoot, "manifest.json").stat();
3042
+ const manifestStat = await require_files.files.file(path, "manifest.json").stat();
2650
3043
  if (manifestStat.type !== "file") missingFiles.push("manifest.json");
3044
+ const installed = missingFiles.length === 0;
2651
3045
  return {
2652
3046
  model,
2653
- modelRoot,
2654
- paths: fluxImageModelPaths(modelRoot),
3047
+ path,
3048
+ paths: fluxImageModelPaths(path, definition.manifest),
2655
3049
  installed,
2656
3050
  verified: installed && invalidFiles.length === 0 && manifestStat.type === "file",
2657
3051
  bytesPresent,
2658
- totalBytes: flux2KleinTotalBytes,
3052
+ totalBytes: definition.totalBytes,
2659
3053
  missingFiles,
2660
3054
  invalidFiles
2661
3055
  };
@@ -2670,8 +3064,8 @@ function tokenArray(value, name) {
2670
3064
  return number;
2671
3065
  });
2672
3066
  }
2673
- async function createFluxTokenizer(modelRoot) {
2674
- const paths = fluxImageModelPaths(modelRoot);
3067
+ async function createFluxTokenizer(path, manifest) {
3068
+ const paths = fluxImageModelPaths(path, manifest);
2675
3069
  const tokenizer = new Tokenizer_default(await require_files.files.file(paths.textEncoderPath, "tokenizer.json").json(), await require_files.files.file(paths.textEncoderPath, "tokenizer_config.json").json());
2676
3070
  const padTokenId = 151643;
2677
3071
  return ({ prompt, maxLength }) => {
@@ -2718,18 +3112,18 @@ function assertTokenArray(value, name) {
2718
3112
  if (value.length === 0) throw new AIError("invalid_message", `${name} must not be empty`);
2719
3113
  for (const token of value) if (!Number.isInteger(token) || token < 0) throw new AIError("invalid_message", `${name} must contain token ids`);
2720
3114
  }
2721
- function imageResult(value, fallbackMetadata) {
2722
- if (!isRecord(value) || typeof value.uri !== "string" || typeof value.width !== "number" || typeof value.height !== "number" || value.model !== "flux2-klein-4b-8bit-abliterated") throw new AIError("image_generation_failed", "Expected generated image URI");
3115
+ function imageResult(value, model, fallbackMetadata) {
3116
+ if (!isRecord(value) || typeof value.uri !== "string" || typeof value.width !== "number" || typeof value.height !== "number" || value.model !== model) throw new AIError("image_generation_failed", "Expected generated image URI");
2723
3117
  const result = value;
2724
3118
  const metadata = isRecord(result.metadata) ? result.metadata : {};
2725
3119
  return {
2726
3120
  uri: value.uri,
2727
3121
  width: value.width,
2728
3122
  height: value.height,
2729
- model: value.model,
3123
+ model,
2730
3124
  metadata: {
2731
3125
  ...fallbackMetadata,
2732
- model: metadata.model === value.model ? metadata.model : fallbackMetadata.model,
3126
+ model: metadata.model === model ? model : fallbackMetadata.model,
2733
3127
  width: typeof metadata.width === "number" ? metadata.width : value.width,
2734
3128
  height: typeof metadata.height === "number" ? metadata.height : value.height,
2735
3129
  steps: typeof metadata.steps === "number" ? metadata.steps : fallbackMetadata.steps,
@@ -2778,15 +3172,18 @@ function nativeErrorMessage(error) {
2778
3172
  return (error instanceof Error ? error.message : String(error)).replace(/^\[(download|tokenize|encode|denoise|decode|write)]\s*/, "");
2779
3173
  }
2780
3174
  async function prepareImageModel(options = {}) {
2781
- const model = options.model ?? flux2Klein4B8BitAbliterated.model;
2782
- assertImageModel(model);
2783
- const root = modelCacheRoot();
3175
+ const definition = imageModelDefinition(options.model ?? flux2Klein4B8BitAbliterated.model);
3176
+ return prepareImageModelAtPath(definition, options, options.directory ? require_files.files.directory(options.directory, definition.manifest.directory).path : modelCacheRoot(definition.manifest).path);
3177
+ }
3178
+ async function prepareImageModelAtPath(definition, options, path) {
3179
+ const model = definition.manifest.model;
3180
+ const root = require_files.files.directory(path);
2784
3181
  await root.create({ recursive: true });
2785
3182
  const headers = options.accessToken ? { Authorization: `Bearer ${options.accessToken}` } : void 0;
2786
- const total = flux2KleinModelFiles.length;
3183
+ const total = definition.files.length;
2787
3184
  const bytesByFile = /* @__PURE__ */ new Map();
2788
3185
  const emitProgress = (progress) => {
2789
- const file = flux2KleinModelFiles.find((candidate) => candidate.destination === progress.file);
3186
+ const file = definition.files.find((candidate) => candidate.destination === progress.file);
2790
3187
  const bytesWritten = file ? Math.min(progress.bytesWritten, file.bytes) : progress.bytesWritten;
2791
3188
  bytesByFile.set(progress.file, bytesWritten);
2792
3189
  const modelBytesWritten = Array.from(bytesByFile.values()).reduce((sum, bytes) => sum + bytes, 0);
@@ -2794,12 +3191,12 @@ async function prepareImageModel(options = {}) {
2794
3191
  ...progress,
2795
3192
  bytesWritten,
2796
3193
  modelBytesWritten,
2797
- totalModelBytes: flux2KleinTotalBytes,
2798
- modelFraction: modelBytesWritten / flux2KleinTotalBytes
3194
+ totalModelBytes: definition.totalBytes,
3195
+ modelFraction: modelBytesWritten / definition.totalBytes
2799
3196
  });
2800
3197
  };
2801
3198
  const missingFiles = [];
2802
- for (const [index, file] of flux2KleinModelFiles.entries()) {
3199
+ for (const [index, file] of definition.files.entries()) {
2803
3200
  const stat = await require_files.files.file(root.path, file.destination).stat();
2804
3201
  if (stat.type === "file" && stat.size === file.bytes) emitProgress({
2805
3202
  model,
@@ -2817,7 +3214,8 @@ async function prepareImageModel(options = {}) {
2817
3214
  }
2818
3215
  await runConcurrent(missingFiles, downloadConcurrency(options.concurrency), async ({ file, index }) => {
2819
3216
  await ensureModelFile({
2820
- modelRoot: root.path,
3217
+ model,
3218
+ path: root.path,
2821
3219
  file,
2822
3220
  headers,
2823
3221
  index,
@@ -2825,22 +3223,26 @@ async function prepareImageModel(options = {}) {
2825
3223
  onProgress: emitProgress
2826
3224
  });
2827
3225
  });
2828
- await writePreparedManifest(root.path);
3226
+ await writePreparedManifest(root.path, definition.manifest);
2829
3227
  return {
2830
3228
  model,
2831
- ...fluxImageModelPaths(root.path),
2832
- tokenizer: await createFluxTokenizer(root.path)
3229
+ ...fluxImageModelPaths(root.path, definition.manifest),
3230
+ tokenizer: await createFluxTokenizer(root.path, definition.manifest)
2833
3231
  };
2834
3232
  }
2835
3233
  async function verifyImageModel(options = {}) {
2836
- return imageModelState(options.model ?? flux2Klein4B8BitAbliterated.model, modelRootFromOptions(options.modelRoot));
3234
+ const definition = imageModelDefinition(options.model ?? flux2Klein4B8BitAbliterated.model);
3235
+ return imageModelState(definition.manifest.model, modelPathFromOptions(definition, options.path));
2837
3236
  }
2838
3237
  async function listImageModels(options = {}) {
2839
- return [await imageModelState(flux2Klein4B8BitAbliterated.model, modelRootFromOptions(options.modelRoot))];
3238
+ return [...await Promise.all([flux2Klein4B8BitAbliterated.model, ternaryBonsaiImage4B.model].map((model) => {
3239
+ const definition = imageModelDefinition(model);
3240
+ return imageModelState(model, options.directory ? require_files.files.directory(options.directory, definition.manifest.directory).path : modelPathFromOptions(definition, void 0));
3241
+ }))];
2840
3242
  }
2841
3243
  async function deleteImageModel(options = {}) {
2842
- assertImageModel(options.model ?? flux2Klein4B8BitAbliterated.model);
2843
- await require_files.files.directory(modelRootFromOptions(options.modelRoot)).delete({ recursive: true });
3244
+ const definition = imageModelDefinition(options.model ?? flux2Klein4B8BitAbliterated.model);
3245
+ await require_files.files.directory(modelPathFromOptions(definition, options.path)).delete({ recursive: true });
2844
3246
  }
2845
3247
  async function imageMemoryTelemetry() {
2846
3248
  const value = await bridge().imageMemoryTelemetry?.();
@@ -2978,8 +3380,8 @@ async function prepareSelectedImageLoRAs(loras, model, options = {}) {
2978
3380
  }
2979
3381
  async function generateImage(options) {
2980
3382
  throwIfAborted(options.signal);
2981
- const model = options.model ?? flux2Klein4B8BitAbliterated.model;
2982
- assertImageModel(model);
3383
+ const definition = imageModelDefinition(options.model ?? flux2Klein4B8BitAbliterated.model);
3384
+ const model = definition.manifest.model;
2983
3385
  const prompt = options.prompt.trim();
2984
3386
  if (!prompt) throw new AIError("invalid_message", "Expected prompt");
2985
3387
  const presetSize = options.aspectRatio ? imageSizeForPreset(options.aspectRatio) : void 0;
@@ -3038,16 +3440,16 @@ async function generateImage(options) {
3038
3440
  const sigmaSchedule = imageSigmaSchedule(selectedLoRAs.map((lora) => lora.metadata?.sigmaSchedule));
3039
3441
  if (sigmaSchedule && options.steps !== void 0 && options.steps !== sigmaSchedule.length) throw new AIError("invalid_model", `steps must equal the LoRA sigma schedule length (${sigmaSchedule.length})`);
3040
3442
  const generationSteps = sigmaSchedule?.length ?? steps;
3041
- const prepared = options.modelRoot && options.tokenizer ? void 0 : await withImageStage("download", () => prepareImageModel({
3443
+ const prepared = options.path && options.tokenizer ? void 0 : await withImageStage("download", () => prepareImageModelAtPath(definition, {
3042
3444
  model,
3043
3445
  accessToken: options.accessToken,
3044
3446
  concurrency: options.concurrency,
3045
3447
  onProgress: options.onModelProgress
3046
- }));
3448
+ }, options.path ?? modelCacheRoot(definition.manifest).path));
3047
3449
  throwIfAborted(options.signal);
3048
3450
  const tokenizer = options.tokenizer ?? prepared?.tokenizer;
3049
- const modelRoot = options.modelRoot ?? prepared?.modelRoot;
3050
- if (!tokenizer || !modelRoot) throw new AIError("invalid_model", "Expected image model root and tokenizer");
3451
+ const modelPath = options.path ?? prepared?.path;
3452
+ if (!tokenizer || !modelPath) throw new AIError("invalid_model", "Expected image model path and tokenizer");
3051
3453
  const tokenized = await withImageStage("tokenize", () => tokenizer({
3052
3454
  prompt,
3053
3455
  maxLength: 512
@@ -3058,7 +3460,7 @@ async function generateImage(options) {
3058
3460
  assertTokenArray(tokenized.attentionMask, "attentionMask");
3059
3461
  if (tokenized.inputIds.length !== tokenized.attentionMask.length) throw new AIError("invalid_message", "inputIds and attentionMask must have the same length");
3060
3462
  });
3061
- const paths = fluxImageModelPaths(modelRoot);
3463
+ const paths = fluxImageModelPaths(modelPath, definition.manifest);
3062
3464
  const request = {
3063
3465
  model,
3064
3466
  prompt,
@@ -3088,7 +3490,10 @@ async function generateImage(options) {
3088
3490
  seed,
3089
3491
  inputIds: tokenized.inputIds,
3090
3492
  attentionMask: tokenized.attentionMask,
3091
- ...paths
3493
+ modelRoot: paths.path,
3494
+ textEncoderPath: paths.textEncoderPath,
3495
+ transformerPath: paths.transformerPath,
3496
+ vaePath: paths.vaePath
3092
3497
  };
3093
3498
  const fallbackMetadata = {
3094
3499
  model,
@@ -3119,7 +3524,7 @@ async function generateImage(options) {
3119
3524
  throwIfAborted(options.signal);
3120
3525
  const value = await native.generateImage?.(request);
3121
3526
  throwIfAborted(options.signal);
3122
- return imageResult(value, fallbackMetadata);
3527
+ return imageResult(value, model, fallbackMetadata);
3123
3528
  } finally {
3124
3529
  subscription?.remove();
3125
3530
  options.signal?.removeEventListener("abort", cancelNative);
@@ -3887,6 +4292,59 @@ function stableStringify(value) {
3887
4292
  return JSON.stringify(value) ?? "undefined";
3888
4293
  }
3889
4294
 
4295
+ //#endregion
4296
+ //#region src/ai/bonsai-generation.ts
4297
+ async function prepareBonsaiGeneration(input) {
4298
+ const state = await verifyBonsaiTextModel(input.model, { path: input.path });
4299
+ if (!state.verified) throw new AIError("model_unavailable", `${input.model.id} is not prepared. Call prepareTextModel() before generation.`);
4300
+ const tokenizer = new Tokenizer_default(await require_files.files.file(state.path, "tokenizer.json").json(), await require_files.files.file(state.path, "tokenizer_config.json").json());
4301
+ const inputIds = tokenizer.encode(formatMessages(messagesFromInput(input))).ids.map(Number);
4302
+ if (!inputIds.length || inputIds.some((token) => !Number.isInteger(token))) throw new AIError("invalid_message", "Tokenizer returned invalid token ids");
4303
+ return {
4304
+ request: {
4305
+ id: input.id,
4306
+ model: input.model.id,
4307
+ modelPath: state.path,
4308
+ inputIds,
4309
+ endTokenIds: endTokenIds(input.model.id),
4310
+ maxTokens: input.maxTokens,
4311
+ temperature: input.temperature,
4312
+ seed: input.seed
4313
+ },
4314
+ decode(tokenIds) {
4315
+ return tokenIds.length ? tokenizer.decode(tokenIds, { skip_special_tokens: true }) : "";
4316
+ }
4317
+ };
4318
+ }
4319
+ function messagesFromInput(input) {
4320
+ if (input.messages) {
4321
+ if (input.system !== void 0 || input.prompt !== void 0) throw new AIError("invalid_message", "messages cannot be combined with system or prompt");
4322
+ if (!input.messages.length) throw new AIError("invalid_message", "Expected at least one message");
4323
+ return input.messages;
4324
+ }
4325
+ if (!input.prompt?.trim()) throw new AIError("invalid_message", "Expected prompt");
4326
+ return [...input.system ? [{
4327
+ role: "system",
4328
+ content: input.system
4329
+ }] : [], {
4330
+ role: "user",
4331
+ content: input.prompt
4332
+ }];
4333
+ }
4334
+ function formatMessages(messages) {
4335
+ if (messages[0]?.role === "assistant") throw new AIError("invalid_message", "Conversation cannot begin with assistant");
4336
+ const parts = messages.map((message, index) => {
4337
+ if (!message.content.trim()) throw new AIError("invalid_message", `Message ${index + 1} is empty`);
4338
+ if (message.role === "system" && index !== 0) throw new AIError("invalid_message", "System message must be first");
4339
+ return `<|im_start|>${message.role}\n${message.content}<|im_end|>\n`;
4340
+ });
4341
+ parts.push("<|im_start|>assistant\n<think>\n\n</think>\n\n");
4342
+ return parts.join("");
4343
+ }
4344
+ function endTokenIds(model) {
4345
+ return model === "ternary-bonsai-27b" ? [248044, 248046] : [151643, 151645];
4346
+ }
4347
+
3890
4348
  //#endregion
3891
4349
  //#region src/ai/text.ts
3892
4350
  function promptFromInput(options) {
@@ -3897,14 +4355,27 @@ function promptFromInput(options) {
3897
4355
  if (parts.length === 0) throw new AIError("invalid_message", "Expected prompt or input");
3898
4356
  return parts.join("\n\n");
3899
4357
  }
3900
- async function isAvailable() {
4358
+ async function isAvailable(model, options = {}) {
4359
+ if (model) return (await availability(model, options)).available;
3901
4360
  try {
3902
4361
  return await bridge().isAvailable?.() ?? false;
3903
4362
  } catch {
3904
4363
  return false;
3905
4364
  }
3906
4365
  }
3907
- async function availability() {
4366
+ async function availability(model, options = {}) {
4367
+ if (model) {
4368
+ const state = await verifyBonsaiTextModel(model, options);
4369
+ if (!state.installed) return {
4370
+ available: false,
4371
+ reason: "not_installed"
4372
+ };
4373
+ if (!state.verified) return {
4374
+ available: false,
4375
+ reason: "invalid_model"
4376
+ };
4377
+ return { available: true };
4378
+ }
3908
4379
  try {
3909
4380
  return await bridge().availability?.() ?? {
3910
4381
  available: false,
@@ -3974,11 +4445,28 @@ async function generate(options) {
3974
4445
  const output = options.output;
3975
4446
  const id = generationId();
3976
4447
  const native = bridge();
3977
- const toolSubscription = listenForTools(id, options.tools);
4448
+ const toolSubscription = listenForTools(id, "tools" in options ? options.tools : void 0);
3978
4449
  const abort = () => void native.cancelTextGeneration?.(id);
3979
4450
  options.signal?.addEventListener("abort", abort, { once: true });
3980
4451
  try {
3981
4452
  if (options.signal?.aborted) throw new AIError("generation_canceled", "Generation canceled");
4453
+ if (options.model) {
4454
+ const prepared = await prepareBonsaiGeneration({
4455
+ id,
4456
+ model: options.model,
4457
+ path: options.path,
4458
+ system: options.system,
4459
+ prompt: options.prompt,
4460
+ messages: options.messages,
4461
+ temperature: options.temperature,
4462
+ maxTokens: options.maxTokens,
4463
+ seed: options.seed
4464
+ });
4465
+ if (options.signal?.aborted) throw new AIError("generation_canceled", "Generation canceled");
4466
+ const tokenIds = await native.generateLocalText?.(prepared.request);
4467
+ if (!tokenIds) throw new AIError("unsupported_runtime", "Local text generation is unavailable in this runtime");
4468
+ return prepared.decode(tokenIds);
4469
+ }
3982
4470
  const value = await native.generate?.(nativeRequest(options, id));
3983
4471
  if (output) {
3984
4472
  const result = output["~standard"].validate(value);
@@ -3996,6 +4484,10 @@ async function generate(options) {
3996
4484
  }
3997
4485
  }
3998
4486
  async function* stream(options) {
4487
+ if (options.model) {
4488
+ yield* streamBonsai(options);
4489
+ return;
4490
+ }
3999
4491
  const id = generationId();
4000
4492
  const native = bridge();
4001
4493
  const updates = [];
@@ -4033,6 +4525,56 @@ async function* stream(options) {
4033
4525
  toolSubscription?.remove();
4034
4526
  }
4035
4527
  }
4528
+ async function* streamBonsai(options) {
4529
+ const id = generationId();
4530
+ const native = bridge();
4531
+ if (options.signal?.aborted) throw new AIError("generation_canceled", "Generation canceled");
4532
+ const prepared = await prepareBonsaiGeneration({
4533
+ id,
4534
+ model: options.model,
4535
+ path: options.path,
4536
+ system: options.system,
4537
+ prompt: options.prompt,
4538
+ messages: options.messages,
4539
+ temperature: options.temperature,
4540
+ maxTokens: options.maxTokens,
4541
+ seed: options.seed
4542
+ });
4543
+ if (options.signal?.aborted) throw new AIError("generation_canceled", "Generation canceled");
4544
+ const updates = [];
4545
+ let wake;
4546
+ let settled = false;
4547
+ let failure;
4548
+ const updateSubscription = native.addListener?.("localTextGenerationUpdate", (event) => {
4549
+ if (event.id !== id) return;
4550
+ updates.push(event.tokenIds);
4551
+ wake?.();
4552
+ wake = void 0;
4553
+ });
4554
+ const abort = () => void native.cancelTextGeneration?.(id);
4555
+ options.signal?.addEventListener("abort", abort, { once: true });
4556
+ const completion = native.streamLocalText?.(prepared.request).catch((error) => {
4557
+ failure = error;
4558
+ return [];
4559
+ }).finally(() => {
4560
+ settled = true;
4561
+ wake?.();
4562
+ wake = void 0;
4563
+ });
4564
+ try {
4565
+ if (!completion) throw new AIError("unsupported_runtime", "Local text streaming is unavailable in this runtime");
4566
+ while (!settled || updates.length > 0) if (updates.length > 0) yield prepared.decode(updates.shift());
4567
+ else await new Promise((resolve) => {
4568
+ wake = resolve;
4569
+ });
4570
+ await completion;
4571
+ if (failure) throw mapNativeError(failure);
4572
+ } finally {
4573
+ if (!settled) await native.cancelTextGeneration?.(id);
4574
+ options.signal?.removeEventListener("abort", abort);
4575
+ updateSubscription?.remove();
4576
+ }
4577
+ }
4036
4578
 
4037
4579
  //#endregion
4038
4580
  //#region src/ai/text-hook.ts
@@ -4106,11 +4648,14 @@ function useTextGeneration() {
4106
4648
  exports.AIError = AIError;
4107
4649
  exports.ImageGenerationError = ImageGenerationError;
4108
4650
  exports.availability = availability;
4651
+ exports.bonsai = bonsai;
4652
+ exports.bonsaiTextModelCatalog = bonsaiTextModelCatalog;
4109
4653
  exports.buildImageGenerationRequest = buildImageGenerationRequest;
4110
4654
  exports.clampOutpaintRect = clampOutpaintRect;
4111
4655
  exports.defaultImageLongSide = defaultImageLongSide;
4112
4656
  exports.deleteImageLoRA = deleteImageLoRA;
4113
4657
  exports.deleteImageModel = deleteImageModel;
4658
+ exports.deleteTextModel = deleteBonsaiTextModel;
4114
4659
  exports.flux2Klein4B8BitAbliterated = flux2Klein4B8BitAbliterated;
4115
4660
  exports.fluxImageModelPaths = fluxImageModelPaths;
4116
4661
  exports.generate = generate;
@@ -4127,15 +4672,18 @@ exports.isAvailable = isAvailable;
4127
4672
  exports.listImageGenerationJobs = listImageGenerationJobs;
4128
4673
  exports.listImageLoRAs = listImageLoRAs;
4129
4674
  exports.listImageModels = listImageModels;
4675
+ exports.listTextModels = listBonsaiTextModels;
4130
4676
  exports.normalizeImageSize = normalizeImageSize;
4131
4677
  exports.prepareImageLoRA = prepareImageLoRA;
4132
4678
  exports.prepareImageModel = prepareImageModel;
4679
+ exports.prepareTextModel = prepareBonsaiTextModel;
4133
4680
  exports.rectForSourceScale = rectForSourceScale;
4134
4681
  exports.roundImageDimension = roundImageDimension;
4135
4682
  exports.roundedImageSize = roundedImageSize;
4136
4683
  exports.scheduleImageGeneration = scheduleImageGeneration;
4137
4684
  exports.shape = shape;
4138
4685
  exports.stream = stream;
4686
+ exports.ternaryBonsaiImage4B = ternaryBonsaiImage4B;
4139
4687
  exports.unloadImageModel = unloadImageModel;
4140
4688
  exports.upscaleImage = upscaleImage;
4141
4689
  exports.useImageComposer = useImageComposer;
@@ -4143,4 +4691,5 @@ exports.useImageGeneration = useImageGeneration;
4143
4691
  exports.useImageModel = useImageModel;
4144
4692
  exports.useTextGeneration = useTextGeneration;
4145
4693
  exports.verifyImageLoRA = verifyImageLoRA;
4146
- exports.verifyImageModel = verifyImageModel;
4694
+ exports.verifyImageModel = verifyImageModel;
4695
+ exports.verifyTextModel = verifyBonsaiTextModel;