bleam 0.0.10 → 0.0.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/dist/ai.cjs +1872 -1182
  2. package/dist/ai.d.cts +267 -234
  3. package/dist/ai.d.ts +267 -234
  4. package/dist/ai.js +1859 -1167
  5. package/dist/cli.cjs +3 -183
  6. package/dist/cli.d.cts +3 -47
  7. package/dist/cli.d.ts +3 -47
  8. package/dist/cli.js +3 -183
  9. package/dist/config.d.cts +1 -1
  10. package/dist/config.d.ts +1 -1
  11. package/dist/{files-Ds1wT8C2.js → files-BXVkPrPN.js} +6 -1
  12. package/dist/{files-Bo7h9fik.cjs → files-DxaQ-Nv0.cjs} +11 -0
  13. package/dist/files.cjs +1 -1
  14. package/dist/files.js +1 -1
  15. package/dist/index.d.cts +1 -1
  16. package/dist/index.d.ts +1 -1
  17. package/dist/platform.cjs +1 -13
  18. package/dist/platform.d.cts +1 -28
  19. package/dist/platform.d.ts +1 -28
  20. package/dist/platform.js +1 -10
  21. package/dist/schema-B5BfdswF.js +226 -0
  22. package/dist/schema-BnVZOXfu.cjs +286 -0
  23. package/dist/schema-D5eImHxu.d.cts +125 -0
  24. package/dist/schema-SSjokbtw.d.ts +125 -0
  25. package/dist/schema.cjs +10 -2
  26. package/dist/schema.d.cts +2 -2
  27. package/dist/schema.d.ts +2 -2
  28. package/dist/schema.js +2 -2
  29. package/dist/state-Dh3HLixb.js +874 -0
  30. package/dist/state-LssDgpff.cjs +973 -0
  31. package/dist/state.cjs +16 -12
  32. package/dist/state.d.cts +144 -140
  33. package/dist/state.d.ts +145 -140
  34. package/dist/state.js +3 -3
  35. package/dist/{ui-Bg11tvlc.d.ts → ui-1WepaMS4.d.cts} +1 -1
  36. package/dist/{ui-Dd7SXdbg.d.cts → ui-D7bRLYee.d.ts} +7 -7
  37. package/dist/ui.d.cts +1 -1
  38. package/dist/ui.d.ts +1 -1
  39. package/dist/window.d.cts +1 -1
  40. package/dist/window.d.ts +1 -1
  41. package/package.json +3 -1
  42. package/templates/foundation-models/app/index.tsx +77 -15
  43. package/templates/image-generation/app/index.tsx +4 -2
  44. package/templates/native/ios/Bleam/AI/Flux2/Configuration/ModelRegistry.swift +1 -1
  45. package/templates/native/ios/Bleam/AI/Flux2/Pipeline/FluxImageRunner.swift +7 -2
  46. package/templates/native/ios/Bleam/AI/TextGeneration/Bonsai/BonsaiTextGenerator.swift +46 -0
  47. package/templates/native/ios/Bleam/AI/TextGeneration/Bonsai/BonsaiTextModel.swift +107 -0
  48. package/templates/native/ios/Bleam/AI/TextGeneration/Bonsai/BonsaiTextSnapshotValidator.swift +201 -0
  49. package/templates/native/ios/Bleam/AI/TextGeneration/Core/GenerationTypes.swift +85 -0
  50. package/templates/native/ios/Bleam/AI/TextGeneration/Core/ModelCapabilities.swift +21 -0
  51. package/templates/native/ios/Bleam/AI/TextGeneration/Core/TextKVCache.swift +23 -0
  52. package/templates/native/ios/Bleam/AI/TextGeneration/Core/TokenSampler.swift +79 -0
  53. package/templates/native/ios/Bleam/AI/TextGeneration/Qwen3/Qwen3CheckpointLoader.swift +161 -0
  54. package/templates/native/ios/Bleam/AI/TextGeneration/Qwen3/Qwen3Configuration.swift +119 -0
  55. package/templates/native/ios/Bleam/AI/TextGeneration/Qwen3/Qwen3InferenceSession.swift +84 -0
  56. package/templates/native/ios/Bleam/AI/TextGeneration/Qwen3/Qwen3Layers.swift +197 -0
  57. package/templates/native/ios/Bleam/AI/TextGeneration/Qwen3/Qwen3Model.swift +130 -0
  58. package/templates/native/ios/Bleam/AI/TextGeneration/Qwen35/Qwen35Checkpoint.swift +102 -0
  59. package/templates/native/ios/Bleam/AI/TextGeneration/Qwen35/Qwen35Configuration.swift +140 -0
  60. package/templates/native/ios/Bleam/AI/TextGeneration/Qwen35/Qwen35Model.swift +112 -0
  61. package/templates/native/ios/Bleam/AI/TextGeneration/Qwen35/Qwen35Session.swift +121 -0
  62. package/templates/native/ios/Bleam/AI/TextGeneration/QwenHybrid/QwenHybridCache.swift +42 -0
  63. package/templates/native/ios/Bleam/AI/TextGeneration/QwenHybrid/QwenHybridFullAttention.swift +57 -0
  64. package/templates/native/ios/Bleam/AI/TextGeneration/QwenHybrid/QwenHybridLayers.swift +58 -0
  65. package/templates/native/ios/Bleam/AI/TextGeneration/QwenHybrid/QwenHybridLinearAttention.swift +111 -0
  66. package/templates/native/ios/Bleam/AppDelegate.swift +0 -206
  67. package/templates/native/ios/Bleam.xcodeproj/project.pbxproj +135 -129
  68. package/templates/native/ios/GenerationService/BonsaiTextGenerationRunner.swift +103 -0
  69. package/templates/native/ios/GenerationService/ImageGenerationRunner.swift +19 -7
  70. package/templates/native/ios/GenerationService/main.swift +174 -48
  71. package/templates/native/ios/Podfile.lock +173 -173
  72. package/templates/native/ios/Shared/Generation/GenerationServiceProtocol.swift +3 -0
  73. package/templates/native/ios/Shared/Generation/GenerationWorkerProtocol.swift +44 -0
  74. package/templates/native/modules/bleam-runtime/ios/AIModule.swift +239 -341
  75. package/templates/native/modules/bleam-runtime/ios/GenerationContracts.swift +47 -0
  76. package/templates/native/modules/bleam-runtime/ios/PlatformModule.swift +0 -191
  77. package/templates/text-generation/app/index.tsx +124 -0
  78. package/templates/text-generation/app.config.ts +5 -0
  79. package/templates/updates/README.md +43 -199
  80. package/templates/updates/src/index.ts +5 -98
  81. package/templates/updates/src/schema.ts +18 -173
  82. package/dist/schema-Bo5Jvqus.js +0 -90
  83. package/dist/schema-DOOjfXvs.d.ts +0 -58
  84. package/dist/schema-ENSMX_1t.d.cts +0 -58
  85. package/dist/schema-rQ13mrpD.cjs +0 -102
  86. package/dist/state-Bx0VlTlO.cjs +0 -852
  87. package/dist/state-CAwe-Vw1.js +0 -767
  88. package/templates/native/ios/PlatformHelper/main.swift +0 -726
  89. /package/dist/{config-Cms0rvqg.d.ts → config-COcRnn5a.d.cts} +0 -0
  90. /package/dist/{config-CufOVJV3.d.cts → config-Chi-flpJ.d.ts} +0 -0
package/dist/ai.js CHANGED
@@ -1,12 +1,58 @@
1
1
  import "./native-runtime-C85Nuc4F.js";
2
2
  import "./crypto-BB92-Upx.js";
3
- import { i as files } from "./files-Ds1wT8C2.js";
4
- import { f as atom, p as useAtom } from "./state-CAwe-Vw1.js";
5
- import "./schema-Bo5Jvqus.js";
3
+ import { a as nativeCopy, i as files } from "./files-BXVkPrPN.js";
4
+ import { c as object, s as number, u as string } from "./schema-B5BfdswF.js";
5
+ import { a as transactionAtom, d as internalJotaiWritableAtom, f as internalStore, g as currentAppStorageKey, i as queryAtom, n as atomCollection, r as mutateAtom, u as internalJotaiAtom } from "./state-Dh3HLixb.js";
6
6
  import { requireNativeModule } from "expo";
7
- import { useRef, useState } from "react";
8
- import { Image } from "react-native";
7
+ import { use, useSyncExternalStore } from "react";
9
8
 
9
+ //#region src/ai/bonsai.ts
10
+ const internalTextModelCatalog = [
11
+ {
12
+ id: "ternary-bonsai-1.7b",
13
+ repo: "prism-ml/Ternary-Bonsai-1.7B-mlx-2bit",
14
+ revision: "5f3e306330f636cfc6c6241b4850fae6711c5985",
15
+ estimatedDownloadBytes: 495528947,
16
+ contextLength: 32768,
17
+ architecture: "qwen3"
18
+ },
19
+ {
20
+ id: "ternary-bonsai-4b",
21
+ repo: "prism-ml/Ternary-Bonsai-4B-mlx-2bit",
22
+ revision: "e1374ad6bf9b1b56afd743936b8faa33c409a75f",
23
+ estimatedDownloadBytes: 1143060044,
24
+ contextLength: 32768,
25
+ architecture: "qwen3"
26
+ },
27
+ {
28
+ id: "ternary-bonsai-8b",
29
+ repo: "prism-ml/Ternary-Bonsai-8B-mlx-2bit",
30
+ revision: "9260b24298e4211e804663e9f519962cf59f34be",
31
+ estimatedDownloadBytes: 2315155948,
32
+ contextLength: 65536,
33
+ architecture: "qwen3"
34
+ },
35
+ {
36
+ id: "ternary-bonsai-27b",
37
+ repo: "prism-ml/Ternary-Bonsai-27B-mlx-2bit",
38
+ revision: "70f75f3ad081ab840a42f3304c02c27e7f89bfb7",
39
+ estimatedDownloadBytes: 8510972679,
40
+ contextLength: 262144,
41
+ architecture: "qwen3.5"
42
+ }
43
+ ];
44
+ const bonsaiTextModelCatalog = Object.freeze(internalTextModelCatalog.map(({ architecture: _,...metadata }) => Object.freeze(metadata)));
45
+ function bonsai(id) {
46
+ const text = internalTextModelCatalog.some((model) => model.id === id);
47
+ if (!text && id !== "ternary-bonsai-image-4b") throw new TypeError(`Unsupported Bonsai model: ${id}`);
48
+ return Object.freeze({
49
+ provider: "bonsai",
50
+ kind: text ? "text" : "image",
51
+ id
52
+ });
53
+ }
54
+
55
+ //#endregion
10
56
  //#region src/ai/errors.ts
11
57
  var AIError = class extends Error {
12
58
  constructor(code, message, cause) {
@@ -47,312 +93,500 @@ function mapNativeError(error) {
47
93
  }
48
94
 
49
95
  //#endregion
50
- //#region src/ai/image-batch.ts
51
- function batchImageSeeds(count, seed) {
52
- if (!Number.isInteger(count) || count < 1 || count > 64) throw new AIError("invalid_message", "Batch count must be an integer from 1 to 64");
53
- const first = seed ?? Math.floor(Math.random() * 4294967296);
54
- return Array.from({ length: count }, (_, index) => first + index >>> 0);
96
+ //#region src/ai/bonsai-text.ts
97
+ const bonsaiTextModelFiles = [
98
+ "config.json",
99
+ "tokenizer.json",
100
+ "tokenizer_config.json",
101
+ "chat_template.jinja",
102
+ "model.safetensors",
103
+ "model.safetensors.index.json"
104
+ ];
105
+ function modelId(model) {
106
+ return typeof model === "string" ? model : model.id;
107
+ }
108
+ function metadataFor(model) {
109
+ const id = modelId(model);
110
+ const metadata = bonsaiTextModelCatalog.find((candidate) => candidate.id === id);
111
+ if (!metadata) throw new AIError("invalid_model", `Unsupported Bonsai text model: ${id}`);
112
+ return metadata;
113
+ }
114
+ function bonsaiTextModelDirectory(model) {
115
+ return files.directory(files.paths.models, "bonsai", "text", modelId(model));
116
+ }
117
+ function modelDirectory(model, override) {
118
+ return override ? files.directory(override) : bonsaiTextModelDirectory(model);
119
+ }
120
+ function modelDirectoryIn(model, directory) {
121
+ return directory ? files.directory(directory, modelId(model)) : bonsaiTextModelDirectory(model);
122
+ }
123
+ function resolveUrl(repo, revision, file) {
124
+ return `https://huggingface.co/${repo.split("/").map(encodeURIComponent).join("/")}/resolve/${encodeURIComponent(revision)}/${encodeURIComponent(file)}`;
125
+ }
126
+ function throwIfAborted$1(signal) {
127
+ if (signal?.aborted) throw new AIError("generation_canceled", "Bonsai model preparation canceled");
128
+ }
129
+ const fileSizes = {
130
+ "ternary-bonsai-1.7b": {
131
+ "config.json": 2939,
132
+ "tokenizer.json": 11422650,
133
+ "tokenizer_config.json": 348,
134
+ "chat_template.jinja": 4063,
135
+ "model.safetensors": 484049216,
136
+ "model.safetensors.index.json": 49731
137
+ },
138
+ "ternary-bonsai-4b": {
139
+ "config.json": 3115,
140
+ "tokenizer.json": 11422650,
141
+ "tokenizer_config.json": 348,
142
+ "chat_template.jinja": 4063,
143
+ "model.safetensors": 1131565944,
144
+ "model.safetensors.index.json": 63924
145
+ },
146
+ "ternary-bonsai-8b": {
147
+ "config.json": 3118,
148
+ "tokenizer.json": 11422650,
149
+ "tokenizer_config.json": 348,
150
+ "chat_template.jinja": 4063,
151
+ "model.safetensors": 2303661704,
152
+ "model.safetensors.index.json": 64065
153
+ },
154
+ "ternary-bonsai-27b": {
155
+ "config.json": 3790,
156
+ "tokenizer.json": 19989343,
157
+ "tokenizer_config.json": 1098,
158
+ "chat_template.jinja": 7764,
159
+ "model.safetensors": 8490785104,
160
+ "model.safetensors.index.json": 185580
161
+ }
162
+ };
163
+ function validFile(model, file, size) {
164
+ return size === fileSizes[model][file];
165
+ }
166
+ async function stateFor(model, rootOverride) {
167
+ const metadata = metadataFor(model);
168
+ const root = modelDirectory(model, rootOverride);
169
+ const missingFiles = [];
170
+ const invalidFiles = [];
171
+ let bytesPresent = 0;
172
+ for (const file of bonsaiTextModelFiles) {
173
+ const stat = await root.file(file).stat();
174
+ if (stat.type !== "file") missingFiles.push(file);
175
+ else {
176
+ bytesPresent += stat.size ?? 0;
177
+ if (!validFile(metadata.id, file, stat.size)) invalidFiles.push(file);
178
+ }
179
+ }
180
+ const expectedRevision = metadata.revision;
181
+ const manifestFile = root.file("manifest.json");
182
+ if ((await manifestFile.stat()).type !== "file") missingFiles.push("manifest.json");
183
+ else try {
184
+ const manifest = await manifestFile.json();
185
+ if (!isRecord(manifest) || manifest.formatVersion !== 1 || manifest.model !== metadata.id || manifest.revision !== expectedRevision) invalidFiles.push("manifest.json");
186
+ } catch {
187
+ invalidFiles.push("manifest.json");
188
+ }
189
+ if (!missingFiles.includes("config.json")) try {
190
+ const config = await root.file("config.json").json();
191
+ if (!validConfig(metadata.id, config)) invalidFiles.push("config.json");
192
+ } catch {
193
+ invalidFiles.push("config.json");
194
+ }
195
+ const installed = missingFiles.length === 0;
196
+ return {
197
+ model: bonsai(metadata.id),
198
+ metadata,
199
+ path: root.path,
200
+ installed,
201
+ verified: installed && invalidFiles.length === 0,
202
+ bytesPresent,
203
+ estimatedTotalBytes: metadata.estimatedDownloadBytes,
204
+ missingFiles,
205
+ invalidFiles
206
+ };
207
+ }
208
+ function validConfig(model, value) {
209
+ if (!isRecord(value)) return false;
210
+ const expected = {
211
+ "ternary-bonsai-1.7b": [
212
+ 2048,
213
+ 28,
214
+ 32768
215
+ ],
216
+ "ternary-bonsai-4b": [
217
+ 2560,
218
+ 36,
219
+ 32768
220
+ ],
221
+ "ternary-bonsai-8b": [
222
+ 4096,
223
+ 36,
224
+ 65536
225
+ ],
226
+ "ternary-bonsai-27b": [
227
+ 5120,
228
+ 64,
229
+ 262144
230
+ ]
231
+ }[model];
232
+ const config = model === "ternary-bonsai-27b" ? value.text_config : value;
233
+ if (!isRecord(config)) return false;
234
+ const quantization = config.quantization;
235
+ 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];
236
+ }
237
+ async function prepareBonsaiTextModel(model, options = {}) {
238
+ const metadata = metadataFor(model);
239
+ const root = modelDirectoryIn(model, options.directory);
240
+ throwIfAborted$1(options.signal);
241
+ await root.create({ recursive: true });
242
+ const headers = options.accessToken ? { Authorization: `Bearer ${options.accessToken}` } : void 0;
243
+ const completedBytes = /* @__PURE__ */ new Map();
244
+ const emit = (file, index, progress) => {
245
+ completedBytes.set(file, progress.bytesWritten);
246
+ const modelBytesWritten = Array.from(completedBytes.values()).reduce((total, bytes) => total + bytes, 0);
247
+ options.onProgress?.({
248
+ model: metadata.id,
249
+ file,
250
+ index,
251
+ total: bonsaiTextModelFiles.length,
252
+ ...progress,
253
+ modelBytesWritten,
254
+ estimatedModelBytes: metadata.estimatedDownloadBytes,
255
+ modelFraction: Math.min(1, modelBytesWritten / metadata.estimatedDownloadBytes)
256
+ });
257
+ };
258
+ for (const [fileIndex, file] of bonsaiTextModelFiles.entries()) {
259
+ throwIfAborted$1(options.signal);
260
+ const index = fileIndex + 1;
261
+ const destination = root.file(file);
262
+ const expectedBytes = fileSizes[metadata.id][file];
263
+ const stat = await destination.stat();
264
+ if (stat.type === "file" && validFile(metadata.id, file, stat.size)) {
265
+ emit(file, index, {
266
+ bytesWritten: stat.size ?? 0,
267
+ totalBytes: stat.size,
268
+ fraction: 1
269
+ });
270
+ continue;
271
+ }
272
+ await destination.download(resolveUrl(metadata.repo, metadata.revision, file), {
273
+ headers,
274
+ expectedBytes,
275
+ atomic: true,
276
+ resume: true,
277
+ signal: options.signal,
278
+ onProgress(progress) {
279
+ emit(file, index, progress);
280
+ }
281
+ }).result;
282
+ throwIfAborted$1(options.signal);
283
+ }
284
+ await root.file("manifest.json").write(JSON.stringify({
285
+ formatVersion: 1,
286
+ model: metadata.id,
287
+ revision: metadata.revision
288
+ }));
289
+ const state = await stateFor(metadata.id, root.path);
290
+ if (!state.verified) throw new AIError("invalid_model", `Downloaded Bonsai model is incomplete: ${[...state.missingFiles, ...state.invalidFiles].join(", ")}`);
291
+ return {
292
+ model: state.model,
293
+ path: root.path
294
+ };
295
+ }
296
+ function verifyBonsaiTextModel(model, options = {}) {
297
+ return stateFor(model, options.path);
298
+ }
299
+ function listBonsaiTextModels(options = {}) {
300
+ return Promise.all(bonsaiTextModelCatalog.map((metadata) => stateFor(metadata.id, options.directory ? files.directory(options.directory, metadata.id).path : void 0)));
301
+ }
302
+ async function deleteBonsaiTextModel(model, options = {}) {
303
+ await modelDirectory(model, options.path).delete({ recursive: true });
55
304
  }
56
305
 
57
306
  //#endregion
58
- //#region src/ai/image-jobs.ts
59
- var ImageGenerationQueue = class {
60
- jobs = /* @__PURE__ */ new Map();
61
- pending = [];
62
- running;
63
- constructor(executor) {
307
+ //#region src/ai/chat-manager.ts
308
+ const missingSnapshot = {
309
+ chat: null,
310
+ messages: []
311
+ };
312
+ var ChatManager = class {
313
+ snapshots = /* @__PURE__ */ new Map();
314
+ chatListeners = /* @__PURE__ */ new Map();
315
+ listListeners = /* @__PURE__ */ new Set();
316
+ controllers = /* @__PURE__ */ new Map();
317
+ listSnapshot = [];
318
+ operation = Promise.resolve();
319
+ ready;
320
+ constructor(repository, executor) {
321
+ this.repository = repository;
64
322
  this.executor = executor;
323
+ this.ready = this.initialize();
65
324
  }
66
325
  setExecutor(executor) {
67
326
  this.executor = executor;
68
327
  }
69
- schedule(options) {
70
- const id = crypto.randomUUID();
71
- let resolve;
72
- let reject;
73
- const promise = new Promise((onResolve, onReject) => {
74
- resolve = onResolve;
75
- reject = onReject;
328
+ async create(input = {}) {
329
+ await this.ready;
330
+ return this.mutate(async () => {
331
+ const chat$1 = await this.repository.create({ title: validTitle(input.title ?? "New Chat") });
332
+ this.set({
333
+ chat: chat$1,
334
+ messages: []
335
+ });
336
+ return chat$1;
76
337
  });
77
- const record = {
78
- snapshot: {
79
- id,
80
- status: "queued",
81
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
82
- },
83
- options,
84
- controller: new AbortController(),
85
- listeners: /* @__PURE__ */ new Set(),
86
- promise,
87
- resolve,
88
- reject
89
- };
90
- this.jobs.set(id, record);
91
- this.pending.push(record);
92
- if (options.signal) {
93
- const cancel = () => this.cancel(id);
94
- options.signal.addEventListener("abort", cancel, { once: true });
95
- record.removeSignalListener = () => options.signal?.removeEventListener("abort", cancel);
96
- if (options.signal.aborted) cancel();
97
- }
98
- this.runNext();
99
- return this.handle(record);
100
- }
101
- get(id) {
102
- const record = this.jobs.get(id);
103
- return record ? this.handle(record) : void 0;
104
338
  }
105
- list() {
106
- return [...this.jobs.values()].map(({ snapshot }) => ({ ...snapshot }));
339
+ async get(id) {
340
+ await this.ready;
341
+ return this.snapshots.get(id)?.chat ?? null;
342
+ }
343
+ async list() {
344
+ await this.ready;
345
+ return this.listSnapshot;
346
+ }
347
+ async send(id, content, options = {}, callbacks = {}) {
348
+ await this.ready;
349
+ if (options.signal?.aborted) throw canceledException();
350
+ const value = content.trim();
351
+ if (!value) throw new AIError("invalid_message", "Message must not be empty");
352
+ const controller = new AbortController();
353
+ const accepted = await this.mutate(async () => {
354
+ if (this.controllers.has(id)) throw new AIError("concurrent_request", "This chat already has an active generation");
355
+ const next = await this.repository.acceptSend(id, value);
356
+ this.controllers.set(id, controller);
357
+ this.setAccepted(next);
358
+ return next;
359
+ });
360
+ this.run(accepted, options, callbacks, controller);
361
+ return resultFromAccepted(accepted);
362
+ }
363
+ async cancel(id) {
364
+ await this.ready;
365
+ return this.mutate(async () => {
366
+ const controller = this.controllers.get(id);
367
+ const assistant = this.snapshots.get(id)?.messages.findLast((message) => message.role === "assistant");
368
+ if (!controller || !assistant || !isActiveMessage(assistant)) return false;
369
+ const next = await this.repository.markCanceling(id, assistant.id);
370
+ this.setAccepted(next);
371
+ controller.abort();
372
+ return true;
373
+ });
107
374
  }
108
- cancel(id) {
109
- const record = this.jobs.get(id);
110
- if (!record || !["queued", "running"].includes(record.snapshot.status)) return false;
111
- record.controller.abort();
112
- if (record.snapshot.status === "queued") {
113
- const index = this.pending.indexOf(record);
114
- if (index >= 0) this.pending.splice(index, 1);
115
- const error = canceledError();
116
- this.update(record, {
117
- status: "canceled",
118
- error
375
+ async retry(id, messageId, options = {}, callbacks = {}) {
376
+ await this.ready;
377
+ if (options.signal?.aborted) throw canceledException();
378
+ const controller = new AbortController();
379
+ const accepted = await this.mutate(async () => {
380
+ if (this.controllers.has(id)) throw new AIError("concurrent_request", "This chat already has an active generation");
381
+ const next = await this.repository.retry(id, messageId);
382
+ this.controllers.set(id, controller);
383
+ this.setAccepted(next);
384
+ return next;
385
+ });
386
+ this.run(accepted, options, callbacks, controller);
387
+ return resultFromAccepted(accepted);
388
+ }
389
+ async setTitle(id, title) {
390
+ await this.ready;
391
+ return this.mutate(async () => {
392
+ const chat$1 = await this.repository.setTitle(id, validTitle(title));
393
+ this.set({
394
+ chat: chat$1,
395
+ messages: this.snapshots.get(id)?.messages ?? []
119
396
  });
120
- record.removeSignalListener?.();
121
- record.reject(error);
122
- }
123
- return true;
397
+ return chat$1;
398
+ });
124
399
  }
125
- handle(record) {
126
- return {
127
- id: record.snapshot.id,
128
- promise: record.promise,
129
- cancel: () => this.cancel(record.snapshot.id),
130
- getSnapshot: () => ({ ...record.snapshot }),
131
- subscribe: (listener) => {
132
- record.listeners.add(listener);
133
- listener({ ...record.snapshot });
134
- return () => record.listeners.delete(listener);
400
+ async remove(id) {
401
+ await this.ready;
402
+ return this.mutate(async () => {
403
+ if (this.controllers.has(id)) throw new AIError("concurrent_request", "Cancel the generation before removing this chat");
404
+ const removed = await this.repository.remove(id);
405
+ if (removed) {
406
+ this.snapshots.delete(id);
407
+ this.refreshList();
408
+ this.emit(id);
135
409
  }
410
+ return removed;
411
+ });
412
+ }
413
+ getChatSnapshot(id) {
414
+ return this.snapshots.get(id) ?? missingSnapshot;
415
+ }
416
+ getListSnapshot() {
417
+ return this.listSnapshot;
418
+ }
419
+ subscribeChat(id, listener) {
420
+ const listeners = this.chatListeners.get(id) ?? /* @__PURE__ */ new Set();
421
+ listeners.add(listener);
422
+ this.chatListeners.set(id, listeners);
423
+ return () => {
424
+ listeners.delete(listener);
425
+ if (listeners.size === 0) this.chatListeners.delete(id);
136
426
  };
137
427
  }
138
- async runNext() {
139
- if (this.running) return;
140
- const record = this.pending.shift();
141
- if (!record) return;
142
- this.running = record;
143
- this.update(record, {
144
- status: "running",
145
- startedAt: (/* @__PURE__ */ new Date()).toISOString()
428
+ subscribeList(listener) {
429
+ this.listListeners.add(listener);
430
+ return () => this.listListeners.delete(listener);
431
+ }
432
+ async initialize() {
433
+ await this.repository.recoverActive();
434
+ const chats$1 = await this.repository.list();
435
+ const transcripts = await Promise.all(chats$1.map((chat$1) => this.repository.messages(chat$1.id)));
436
+ chats$1.forEach((chat$1, index) => {
437
+ this.snapshots.set(chat$1.id, {
438
+ chat: chat$1,
439
+ messages: transcripts[index] ?? []
440
+ });
146
441
  });
442
+ this.refreshList();
443
+ }
444
+ run(accepted, options, callbacks, controller) {
445
+ const externalAbort = () => void this.cancel(accepted.chatId);
446
+ options.signal?.addEventListener("abort", externalAbort, { once: true });
447
+ if (options.signal?.aborted) externalAbort();
448
+ this.generate(accepted, options, callbacks, controller).finally(() => {
449
+ options.signal?.removeEventListener("abort", externalAbort);
450
+ }).catch(() => void 0);
451
+ }
452
+ async generate(accepted, options, callbacks, controller) {
453
+ let current = accepted.assistantMessage;
454
+ let content = "";
455
+ let lastPersistedAt = 0;
147
456
  try {
148
- const result = await this.executor({
149
- ...record.options,
150
- signal: record.controller.signal,
151
- onProgress: (progress) => {
152
- record.options.onProgress?.(progress);
153
- this.progress(record, progress);
154
- }
457
+ const started = await this.mutate(async () => {
458
+ const next = await this.repository.start(accepted.chatId, accepted.assistantMessageId);
459
+ this.setAccepted(next);
460
+ return next;
155
461
  });
156
- this.update(record, {
157
- status: "completed",
158
- result,
159
- completedAt: (/* @__PURE__ */ new Date()).toISOString()
462
+ current = started.assistantMessage;
463
+ const messages = historyBefore(started.messages, current, options.system);
464
+ const executorOptions = {
465
+ ...options,
466
+ messages,
467
+ signal: controller.signal
468
+ };
469
+ delete executorOptions.system;
470
+ for await (const snapshot of this.executor.stream(executorOptions)) {
471
+ content = snapshot;
472
+ await this.mutate(async () => {
473
+ current = {
474
+ ...current,
475
+ content,
476
+ updatedAt: now$1()
477
+ };
478
+ this.replaceMessage(started.chatId, current);
479
+ const timestamp$1 = Date.now();
480
+ if (timestamp$1 - lastPersistedAt >= 100) {
481
+ current = await this.repository.updateContent(current);
482
+ lastPersistedAt = timestamp$1;
483
+ }
484
+ });
485
+ }
486
+ await this.mutate(async () => {
487
+ const next = await this.repository.finish(started.chatId, current.id, controller.signal.aborted ? "canceled" : "completed", content, controller.signal.aborted ? canceledError$1() : void 0);
488
+ this.controllers.delete(started.chatId);
489
+ this.setAccepted(next);
490
+ if (next.assistantMessage.status === "completed") notify(callbacks.onFinish, next.assistantMessage);
491
+ else notify(callbacks.onError, next.assistantMessage.error);
160
492
  });
161
- record.resolve(result);
162
493
  } catch (error) {
163
- const canceled = record.controller.signal.aborted;
164
- const failure = canceled ? canceledError(error) : error;
165
- this.update(record, {
166
- status: canceled ? "canceled" : "failed",
167
- error: failure,
168
- completedAt: (/* @__PURE__ */ new Date()).toISOString()
494
+ const serialized = controller.signal.aborted ? canceledError$1() : serializeError(error);
495
+ await this.mutate(async () => {
496
+ const next = await this.repository.finish(accepted.chatId, accepted.assistantMessageId, controller.signal.aborted ? "canceled" : "failed", content, serialized);
497
+ this.controllers.delete(accepted.chatId);
498
+ this.setAccepted(next);
499
+ notify(callbacks.onError, serialized);
169
500
  });
170
- record.reject(failure);
171
501
  } finally {
172
- record.removeSignalListener?.();
173
- this.running = void 0;
174
- this.runNext();
502
+ if (this.controllers.get(accepted.chatId) === controller) this.controllers.delete(accepted.chatId);
175
503
  }
176
504
  }
177
- progress(record, progress) {
178
- this.update(record, { progress });
505
+ mutate(action) {
506
+ const result = this.operation.then(action, action);
507
+ this.operation = result.then(() => void 0, () => void 0);
508
+ return result;
179
509
  }
180
- update(record, update) {
181
- record.snapshot = {
182
- ...record.snapshot,
183
- ...update
184
- };
185
- for (const listener of record.listeners) listener({ ...record.snapshot });
510
+ setAccepted(value) {
511
+ this.set({
512
+ chat: value.chat,
513
+ messages: value.messages
514
+ });
186
515
  }
187
- };
188
- function canceledError(cause) {
189
- return new AIError("generation_canceled", "Image generation canceled", cause);
190
- }
191
-
192
- //#endregion
193
- //#region src/ai/image-models.ts
194
- const flux2Klein4B8BitAbliterated = {
195
- formatVersion: 1,
196
- model: "flux2-klein-4b-8bit-abliterated",
197
- directory: "flux2-klein-4b-8bit-abliterated",
198
- metadata: {
199
- displayName: "Flux.2 Klein 4B Abliterated",
200
- family: "flux2-klein",
201
- precision: "8-bit",
202
- estimatedMemoryGB: 12,
203
- quality: "high",
204
- speed: "fast",
205
- capabilities: [
206
- "text-to-image",
207
- "image-to-image",
208
- "inpainting",
209
- "outpainting",
210
- "references",
211
- "lora"
212
- ]
213
- },
214
- components: {
215
- textEncoder: {
216
- repo: "mlx-community/Josiefied-Qwen3-4B-abliterated-v1-8bit",
217
- revision: "main",
218
- path: "text-encoder",
219
- files: [
220
- "config.json",
221
- "tokenizer.json",
222
- "tokenizer_config.json",
223
- "*.safetensors",
224
- "*.safetensors.index.json"
225
- ]
226
- },
227
- transformer: {
228
- repo: "moxin-org/FLUX.2-klein-4B-8bit-mlx",
229
- revision: "main",
230
- path: "transformer",
231
- files: ["config.json", "quantized_8bit.safetensors"]
232
- },
233
- vae: {
234
- repo: "black-forest-labs/FLUX.2-klein-4B",
235
- revision: "main",
236
- path: "vae",
237
- files: [
238
- "config.json",
239
- "diffusion_pytorch_model.safetensors",
240
- "diffusion_pytorch_model.safetensors.index.json",
241
- "diffusion_pytorch_model-*.safetensors"
242
- ]
243
- }
516
+ replaceMessage(chatId, message) {
517
+ const snapshot = this.snapshots.get(chatId);
518
+ if (!snapshot) return;
519
+ this.set({
520
+ chat: snapshot.chat,
521
+ messages: snapshot.messages.map((item) => item.id === message.id ? message : item)
522
+ });
523
+ }
524
+ set(snapshot) {
525
+ if (!snapshot.chat) return;
526
+ const previous = this.snapshots.get(snapshot.chat.id);
527
+ this.snapshots.set(snapshot.chat.id, snapshot);
528
+ if (previous?.chat !== snapshot.chat) this.refreshList();
529
+ this.emit(snapshot.chat.id);
530
+ }
531
+ refreshList() {
532
+ this.listSnapshot = [...this.snapshots.values()].flatMap((snapshot) => snapshot.chat ? [snapshot.chat] : []).sort((left, right) => left.updatedAt === right.updatedAt ? right.id.localeCompare(left.id) : right.updatedAt.localeCompare(left.updatedAt));
533
+ for (const listener of this.listListeners) listener();
534
+ }
535
+ emit(id) {
536
+ for (const listener of this.chatListeners.get(id) ?? []) listener();
244
537
  }
245
538
  };
246
- const imageModelManifests = [flux2Klein4B8BitAbliterated];
247
- function imageModelManifest(model) {
248
- return imageModelManifests.find((manifest) => manifest.model === model);
539
+ function historyBefore(messages, target, system) {
540
+ return [...system?.trim() ? [{
541
+ role: "system",
542
+ content: system
543
+ }] : [], ...messages.filter((message) => message.sequence < target.sequence && (message.role === "user" || message.status === "completed")).map((message) => ({
544
+ role: message.role,
545
+ content: message.content
546
+ }))];
249
547
  }
250
- const flux2KleinModelFiles = [
251
- {
252
- repo: flux2Klein4B8BitAbliterated.components.textEncoder.repo,
253
- revision: flux2Klein4B8BitAbliterated.components.textEncoder.revision,
254
- source: "config.json",
255
- destination: "text-encoder/config.json",
256
- bytes: 937
257
- },
258
- {
259
- repo: flux2Klein4B8BitAbliterated.components.textEncoder.repo,
260
- revision: flux2Klein4B8BitAbliterated.components.textEncoder.revision,
261
- source: "tokenizer.json",
262
- destination: "text-encoder/tokenizer.json",
263
- bytes: 11422654
264
- },
265
- {
266
- repo: flux2Klein4B8BitAbliterated.components.textEncoder.repo,
267
- revision: flux2Klein4B8BitAbliterated.components.textEncoder.revision,
268
- source: "tokenizer_config.json",
269
- destination: "text-encoder/tokenizer_config.json",
270
- bytes: 9706
271
- },
272
- {
273
- repo: flux2Klein4B8BitAbliterated.components.textEncoder.repo,
274
- revision: flux2Klein4B8BitAbliterated.components.textEncoder.revision,
275
- source: "model.safetensors",
276
- destination: "text-encoder/model.safetensors",
277
- bytes: 4274158989
278
- },
279
- {
280
- repo: flux2Klein4B8BitAbliterated.components.textEncoder.repo,
281
- revision: flux2Klein4B8BitAbliterated.components.textEncoder.revision,
282
- source: "model.safetensors.index.json",
283
- destination: "text-encoder/model.safetensors.index.json",
284
- bytes: 63924
285
- },
286
- {
287
- repo: flux2Klein4B8BitAbliterated.components.transformer.repo,
288
- revision: flux2Klein4B8BitAbliterated.components.transformer.revision,
289
- source: "transformer/config.json",
290
- destination: "transformer/config.json",
291
- bytes: 541
292
- },
293
- {
294
- repo: flux2Klein4B8BitAbliterated.components.transformer.repo,
295
- revision: flux2Klein4B8BitAbliterated.components.transformer.revision,
296
- source: "transformer/quantized_8bit.safetensors",
297
- destination: "transformer/quantized_8bit.safetensors",
298
- bytes: 4360050983
299
- },
300
- {
301
- repo: flux2Klein4B8BitAbliterated.components.vae.repo,
302
- revision: flux2Klein4B8BitAbliterated.components.vae.revision,
303
- source: "vae/config.json",
304
- destination: "vae/config.json",
305
- bytes: 821
306
- },
307
- {
308
- repo: flux2Klein4B8BitAbliterated.components.vae.repo,
309
- revision: flux2Klein4B8BitAbliterated.components.vae.revision,
310
- source: "vae/diffusion_pytorch_model.safetensors",
311
- destination: "vae/diffusion_pytorch_model.safetensors",
312
- bytes: 168120878
313
- }
314
- ];
315
- const flux2KleinTotalBytes = flux2KleinModelFiles.reduce((total, file) => total + file.bytes, 0);
316
- function joinModelPath(root, component) {
317
- return `${root.replace(/\/+$/, "")}/${component.replace(/^\/+/, "")}`;
548
+ function isActiveMessage(message) {
549
+ return message.role === "assistant" && (message.status === "queued" || message.status === "generating" || message.status === "canceling");
550
+ }
551
+ function resultFromAccepted(value) {
552
+ return {
553
+ chatId: value.chatId,
554
+ userMessageId: value.userMessageId,
555
+ assistantMessageId: value.assistantMessageId
556
+ };
557
+ }
558
+ function validTitle(title) {
559
+ const value = title.trim();
560
+ if (!value) throw new AIError("invalid_message", "Title must not be empty");
561
+ return value;
318
562
  }
319
- function fluxImageModelPaths(modelRoot, manifest = flux2Klein4B8BitAbliterated) {
563
+ function serializeError(error) {
564
+ const value = error instanceof Error ? error : new Error(String(error));
565
+ const details = value;
320
566
  return {
321
- modelRoot,
322
- textEncoderPath: joinModelPath(modelRoot, manifest.components.textEncoder.path),
323
- transformerPath: joinModelPath(modelRoot, manifest.components.transformer.path),
324
- vaePath: joinModelPath(modelRoot, manifest.components.vae.path)
567
+ name: value.name,
568
+ message: value.message,
569
+ code: typeof details.code === "string" ? details.code : void 0,
570
+ stage: typeof details.stage === "string" ? details.stage : void 0
325
571
  };
326
572
  }
327
-
328
- //#endregion
329
- //#region src/ai/image-schedule.ts
330
- function imageSigmaSchedule(schedules) {
331
- const configured = schedules.filter((schedule) => schedule !== void 0);
332
- if (configured.length === 0) return void 0;
333
- const first = configured[0];
334
- if (first.length === 0 || first.some((sigma, index) => !Number.isFinite(sigma) || sigma <= 0 || sigma > 1 || index > 0 && sigma >= first[index - 1])) throw new AIError("invalid_model", "LoRA sigmaSchedule must contain descending values greater than 0 and at most 1");
335
- if (configured.some((schedule) => schedule.length !== first.length || schedule.some((sigma, index) => sigma !== first[index]))) throw new AIError("invalid_model", "Selected LoRAs require conflicting sigma schedules");
336
- return [...first];
337
- }
338
-
339
- //#endregion
340
- //#region src/ai/image-upscale.ts
341
- function normalizeUpscaleScale(value) {
342
- const scale = value ?? 2;
343
- if (!Number.isFinite(scale) || scale <= 1 || scale > 4) throw new AIError("invalid_model", "Upscale scale must be greater than 1 and at most 4");
344
- return scale;
345
- }
346
-
347
- //#endregion
348
- //#region src/ai/native.ts
349
- function bridge() {
350
- try {
351
- return requireNativeModule("AI");
352
- } catch (error) {
353
- throw new AIError("unsupported_runtime", "bleam/ai requires the Bleam native runtime", error);
354
- }
355
- }
573
+ function canceledError$1() {
574
+ return {
575
+ name: "AIError",
576
+ code: "generation_canceled",
577
+ message: "Generation canceled"
578
+ };
579
+ }
580
+ function canceledException() {
581
+ return new AIError("generation_canceled", "Generation canceled");
582
+ }
583
+ function notify(callback, value) {
584
+ if (!callback) return;
585
+ Promise.resolve().then(() => callback(value)).catch(() => void 0);
586
+ }
587
+ function now$1() {
588
+ return (/* @__PURE__ */ new Date()).toISOString();
589
+ }
356
590
 
357
591
  //#endregion
358
592
  //#region src/ai/tokenizers.mjs
@@ -492,7 +726,7 @@ var create_pattern = (pattern, invert = true) => {
492
726
  return null;
493
727
  }
494
728
  };
495
- var escape_reg_exp = (string) => string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
729
+ var escape_reg_exp = (string$1) => string$1.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
496
730
  var fuse_unk = (arr, tokens_to_ids, unk_token_id) => {
497
731
  const fused = [];
498
732
  let i = 0;
@@ -2214,16 +2448,16 @@ var ByteFallback = class extends Decoder_default {
2214
2448
  if (bytes !== null) previous_byte_tokens.push(bytes);
2215
2449
  else {
2216
2450
  if (previous_byte_tokens.length > 0) {
2217
- const string = this.text_decoder.decode(Uint8Array.from(previous_byte_tokens));
2218
- new_tokens.push(string);
2451
+ const string$1 = this.text_decoder.decode(Uint8Array.from(previous_byte_tokens));
2452
+ new_tokens.push(string$1);
2219
2453
  previous_byte_tokens = [];
2220
2454
  }
2221
2455
  new_tokens.push(token);
2222
2456
  }
2223
2457
  }
2224
2458
  if (previous_byte_tokens.length > 0) {
2225
- const string = this.text_decoder.decode(Uint8Array.from(previous_byte_tokens));
2226
- new_tokens.push(string);
2459
+ const string$1 = this.text_decoder.decode(Uint8Array.from(previous_byte_tokens));
2460
+ new_tokens.push(string$1);
2227
2461
  previous_byte_tokens = [];
2228
2462
  }
2229
2463
  return new_tokens;
@@ -2418,16 +2652,1266 @@ var Tokenizer = class {
2418
2652
  };
2419
2653
  var Tokenizer_default = Tokenizer;
2420
2654
 
2655
+ //#endregion
2656
+ //#region src/ai/bonsai-generation.ts
2657
+ async function prepareBonsaiGeneration(input) {
2658
+ const state = await verifyBonsaiTextModel(input.model, { path: input.path });
2659
+ if (!state.verified) throw new AIError("model_unavailable", `${input.model.id} is not prepared. Call prepareTextModel() before generation.`);
2660
+ const tokenizer = new Tokenizer_default(await files.file(state.path, "tokenizer.json").json(), await files.file(state.path, "tokenizer_config.json").json());
2661
+ const inputIds = tokenizer.encode(formatMessages(messagesFromInput(input))).ids.map(Number);
2662
+ if (!inputIds.length || inputIds.some((token) => !Number.isInteger(token))) throw new AIError("invalid_message", "Tokenizer returned invalid token ids");
2663
+ return {
2664
+ request: {
2665
+ id: input.id,
2666
+ model: input.model.id,
2667
+ modelPath: state.path,
2668
+ inputIds,
2669
+ endTokenIds: endTokenIds(input.model.id),
2670
+ maxTokens: input.maxTokens,
2671
+ temperature: input.temperature,
2672
+ seed: input.seed
2673
+ },
2674
+ decode(tokenIds) {
2675
+ return tokenIds.length ? tokenizer.decode(tokenIds, { skip_special_tokens: true }) : "";
2676
+ }
2677
+ };
2678
+ }
2679
+ function messagesFromInput(input) {
2680
+ if (input.messages) {
2681
+ if (input.system !== void 0 || input.prompt !== void 0) throw new AIError("invalid_message", "messages cannot be combined with system or prompt");
2682
+ if (!input.messages.length) throw new AIError("invalid_message", "Expected at least one message");
2683
+ return input.messages;
2684
+ }
2685
+ if (!input.prompt?.trim()) throw new AIError("invalid_message", "Expected prompt");
2686
+ return [...input.system ? [{
2687
+ role: "system",
2688
+ content: input.system
2689
+ }] : [], {
2690
+ role: "user",
2691
+ content: input.prompt
2692
+ }];
2693
+ }
2694
+ function formatMessages(messages) {
2695
+ if (messages[0]?.role === "assistant") throw new AIError("invalid_message", "Conversation cannot begin with assistant");
2696
+ const parts = messages.map((message, index) => {
2697
+ if (!message.content.trim()) throw new AIError("invalid_message", `Message ${index + 1} is empty`);
2698
+ if (message.role === "system" && index !== 0) throw new AIError("invalid_message", "System message must be first");
2699
+ return `<|im_start|>${message.role}\n${message.content}<|im_end|>\n`;
2700
+ });
2701
+ parts.push("<|im_start|>assistant\n<think>\n\n</think>\n\n");
2702
+ return parts.join("");
2703
+ }
2704
+ function endTokenIds(model) {
2705
+ return model === "ternary-bonsai-27b" ? [248044, 248046] : [151643, 151645];
2706
+ }
2707
+
2708
+ //#endregion
2709
+ //#region src/ai/native.ts
2710
+ function bridge() {
2711
+ try {
2712
+ return requireNativeModule("AI");
2713
+ } catch (error) {
2714
+ throw new AIError("unsupported_runtime", "bleam/ai requires the Bleam native runtime", error);
2715
+ }
2716
+ }
2717
+
2718
+ //#endregion
2719
+ //#region src/ai/chat-executor.ts
2720
+ function generationId() {
2721
+ return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
2722
+ }
2723
+ function foundationRequest(options, id) {
2724
+ if (options.temperature !== void 0 && (!Number.isFinite(options.temperature) || options.temperature < 0)) throw new AIError("invalid_message", "temperature must be a non-negative number");
2725
+ if (options.maxTokens !== void 0 && (!Number.isInteger(options.maxTokens) || options.maxTokens <= 0)) throw new AIError("invalid_message", "maxTokens must be a positive integer");
2726
+ if (options.messages.length === 0) throw new AIError("invalid_message", "Expected at least one message");
2727
+ let system;
2728
+ const conversation = [];
2729
+ for (const [index, message] of options.messages.entries()) {
2730
+ if (!message.content.trim()) throw new AIError("invalid_message", `Message ${index + 1} is empty`);
2731
+ if (message.role === "system") {
2732
+ if (index !== 0 || system !== void 0) throw new AIError("invalid_message", "System message must be first");
2733
+ system = message.content;
2734
+ } else conversation.push({
2735
+ role: message.role,
2736
+ content: message.content
2737
+ });
2738
+ }
2739
+ if (conversation[0]?.role === "assistant") throw new AIError("invalid_message", "Conversation cannot begin with assistant");
2740
+ const final = conversation.at(-1);
2741
+ if (final?.role !== "user") throw new AIError("invalid_message", "Conversation must end with a user message");
2742
+ return {
2743
+ id,
2744
+ system,
2745
+ prompt: final.content,
2746
+ messages: conversation.slice(0, -1),
2747
+ temperature: options.temperature,
2748
+ maxTokens: options.maxTokens
2749
+ };
2750
+ }
2751
+ async function* streamChat(options) {
2752
+ if (options.model) {
2753
+ yield* streamBonsaiChat(options);
2754
+ return;
2755
+ }
2756
+ const id = generationId();
2757
+ const native = bridge();
2758
+ const updates = [];
2759
+ let wake;
2760
+ let settled = false;
2761
+ let failure;
2762
+ const updateSubscription = native.addListener?.("textGenerationUpdate", (event) => {
2763
+ if (event.id !== id) return;
2764
+ updates.push(event.content);
2765
+ wake?.();
2766
+ wake = void 0;
2767
+ });
2768
+ const abort = () => void native.cancelTextGeneration?.(id);
2769
+ options.signal.addEventListener("abort", abort, { once: true });
2770
+ const completion = native.streamGenerate?.(foundationRequest(options, id)).catch((error) => {
2771
+ failure = error;
2772
+ }).finally(() => {
2773
+ settled = true;
2774
+ wake?.();
2775
+ wake = void 0;
2776
+ });
2777
+ try {
2778
+ if (options.signal.aborted) {
2779
+ abort();
2780
+ throw new AIError("generation_canceled", "Generation canceled");
2781
+ }
2782
+ if (!completion) throw new AIError("unsupported_runtime", "Streaming is unavailable in this runtime");
2783
+ while (!settled || updates.length > 0) if (updates.length > 0) yield updates.shift();
2784
+ else await new Promise((resolve) => {
2785
+ wake = resolve;
2786
+ });
2787
+ await completion;
2788
+ if (failure) throw mapNativeError(failure);
2789
+ } finally {
2790
+ if (!settled) await native.cancelTextGeneration?.(id);
2791
+ options.signal.removeEventListener("abort", abort);
2792
+ updateSubscription?.remove();
2793
+ }
2794
+ }
2795
+ async function* streamBonsaiChat(options) {
2796
+ const id = generationId();
2797
+ const native = bridge();
2798
+ if (options.signal.aborted) throw new AIError("generation_canceled", "Generation canceled");
2799
+ const prepared = await prepareBonsaiGeneration({
2800
+ id,
2801
+ model: options.model,
2802
+ path: options.path,
2803
+ messages: options.messages,
2804
+ temperature: options.temperature,
2805
+ maxTokens: options.maxTokens,
2806
+ seed: options.seed
2807
+ });
2808
+ if (options.signal.aborted) throw new AIError("generation_canceled", "Generation canceled");
2809
+ const updates = [];
2810
+ let wake;
2811
+ let settled = false;
2812
+ let failure;
2813
+ const updateSubscription = native.addListener?.("localTextGenerationUpdate", (event) => {
2814
+ if (event.id !== id) return;
2815
+ updates.push(event.tokenIds);
2816
+ wake?.();
2817
+ wake = void 0;
2818
+ });
2819
+ const abort = () => void native.cancelTextGeneration?.(id);
2820
+ options.signal.addEventListener("abort", abort, { once: true });
2821
+ const completion = native.streamLocalText?.(prepared.request).catch((error) => {
2822
+ failure = error;
2823
+ return [];
2824
+ }).finally(() => {
2825
+ settled = true;
2826
+ wake?.();
2827
+ wake = void 0;
2828
+ });
2829
+ try {
2830
+ if (!completion) throw new AIError("unsupported_runtime", "Local text streaming is unavailable in this runtime");
2831
+ while (!settled || updates.length > 0) if (updates.length > 0) yield prepared.decode(updates.shift());
2832
+ else await new Promise((resolve) => {
2833
+ wake = resolve;
2834
+ });
2835
+ await completion;
2836
+ if (failure) throw mapNativeError(failure);
2837
+ } finally {
2838
+ if (!settled) await native.cancelTextGeneration?.(id);
2839
+ options.signal.removeEventListener("abort", abort);
2840
+ updateSubscription?.remove();
2841
+ }
2842
+ }
2843
+
2844
+ //#endregion
2845
+ //#region src/ai/chat.tsx
2846
+ const chatShape = {
2847
+ title: string({ default: "New Chat" }),
2848
+ status: string({ default: "ready" })
2849
+ };
2850
+ const errorShape = {
2851
+ name: string(),
2852
+ message: string(),
2853
+ code: string(),
2854
+ stage: string()
2855
+ };
2856
+ const messageShape = {
2857
+ chatId: string(),
2858
+ role: string(),
2859
+ status: string(),
2860
+ content: string({ default: "" }),
2861
+ attempt: number({ default: 1 }),
2862
+ sequence: number(),
2863
+ error: object(errorShape),
2864
+ startedAt: string(),
2865
+ completedAt: string()
2866
+ };
2867
+ const chats = atomCollection("__bleam_ai_chats", chatShape);
2868
+ const chatMessages = atomCollection("__bleam_ai_chat_messages", messageShape);
2869
+ var CollectionChatRepository = class {
2870
+ async create(input) {
2871
+ const insert = mutateAtom(chats, (mutation) => mutation.insert());
2872
+ const id = await internalStore.set(internalJotaiWritableAtom(insert), {
2873
+ title: input.title ?? "New Chat",
2874
+ status: "ready"
2875
+ });
2876
+ return this.requireChat(id);
2877
+ }
2878
+ async get(id) {
2879
+ const row = queryAtom(chats, (query) => query.row(id));
2880
+ return chatFromRow(await internalStore.get(internalJotaiAtom(row)));
2881
+ }
2882
+ async list() {
2883
+ const ids = queryAtom(chats, (query) => query.sortBy("updatedAt", "desc").ids());
2884
+ const values = await internalStore.get(internalJotaiAtom(ids));
2885
+ return Promise.all(values.map((id) => this.requireChat(id)));
2886
+ }
2887
+ async messages(chatId) {
2888
+ const ids = queryAtom(chatMessages, (query) => query.where({ chatId }).sortBy("sequence").ids());
2889
+ const values = await internalStore.get(internalJotaiAtom(ids));
2890
+ return (await Promise.all(values.map((id) => this.messageRow(id)))).flatMap((row) => row ? [messageFromRow(row)] : []);
2891
+ }
2892
+ async acceptSend(chatId, content) {
2893
+ const transaction = transactionAtom(async (tx) => {
2894
+ const chat$1 = await tx.get(chats, chatId);
2895
+ if (!chat$1) throw new Error(`Chat not found: ${chatId}`);
2896
+ if (isActiveStatus(chat$1.data.status)) throw new Error("This chat already has an active generation");
2897
+ const previousIds = await tx.query(chatMessages, (query) => query.where({ chatId }).sortBy("sequence", "desc").take(1).ids());
2898
+ const sequence = ((previousIds[0] ? await tx.get(chatMessages, previousIds[0]) : null)?.data.sequence ?? 0) + 1;
2899
+ const userId = await tx.insert(chatMessages, {
2900
+ chatId,
2901
+ role: "user",
2902
+ status: "completed",
2903
+ content,
2904
+ attempt: 1,
2905
+ sequence,
2906
+ completedAt: timestamp()
2907
+ });
2908
+ const assistantId = await tx.insert(chatMessages, {
2909
+ chatId,
2910
+ role: "assistant",
2911
+ status: "queued",
2912
+ content: "",
2913
+ attempt: 1,
2914
+ sequence: sequence + 1
2915
+ });
2916
+ return acceptedFromTransaction(tx, await tx.update(chats, chatId, { status: "queued" }), assistantId, userId);
2917
+ });
2918
+ return internalStore.set(internalJotaiWritableAtom(transaction));
2919
+ }
2920
+ async start(chatId, messageId) {
2921
+ return this.transition(chatId, messageId, async (tx, _chat, message) => {
2922
+ if (message.data.status !== "queued") throw new Error("Only queued messages can start");
2923
+ const startedAt = timestamp();
2924
+ const assistant = await tx.update(chatMessages, messageId, {
2925
+ status: "generating",
2926
+ startedAt
2927
+ });
2928
+ return acceptedFromTransaction(tx, await tx.update(chats, chatId, { status: "generating" }), assistant.id);
2929
+ });
2930
+ }
2931
+ async updateContent(message) {
2932
+ const update = mutateAtom(chatMessages, (mutation) => mutation.update(message.id));
2933
+ return messageFromRow(await internalStore.set(internalJotaiWritableAtom(update), { content: message.content }));
2934
+ }
2935
+ async markCanceling(chatId, messageId) {
2936
+ return this.transition(chatId, messageId, async (tx, _chat, message) => {
2937
+ if (!isActiveStatus(message.data.status ?? "")) throw new Error("Message is not active");
2938
+ const assistant = await tx.update(chatMessages, messageId, { status: "canceling" });
2939
+ return acceptedFromTransaction(tx, await tx.update(chats, chatId, { status: "canceling" }), assistant.id);
2940
+ });
2941
+ }
2942
+ async finish(chatId, messageId, status, content, error) {
2943
+ return this.transition(chatId, messageId, async (tx, _chat) => {
2944
+ const completedAt = timestamp();
2945
+ const assistant = await tx.update(chatMessages, messageId, {
2946
+ status,
2947
+ content,
2948
+ error,
2949
+ completedAt
2950
+ });
2951
+ return acceptedFromTransaction(tx, await tx.update(chats, chatId, { status: status === "completed" ? "ready" : status }), assistant.id);
2952
+ });
2953
+ }
2954
+ async retry(chatId, messageId) {
2955
+ return this.transition(chatId, messageId, async (tx, _chat, message) => {
2956
+ const ids = await tx.query(chatMessages, (query) => query.where({ chatId }).sortBy("sequence", "desc").ids());
2957
+ const rows = await Promise.all(ids.map((id) => tx.get(chatMessages, id)));
2958
+ if (rows.find((row) => row?.data.role === "assistant")?.id !== messageId) throw new Error("Only the latest assistant message can be retried");
2959
+ if (message.data.status !== "failed" && message.data.status !== "canceled") throw new Error("Only failed or canceled messages can be retried");
2960
+ const assistant = await tx.update(chatMessages, messageId, {
2961
+ status: "queued",
2962
+ content: "",
2963
+ attempt: message.data.attempt + 1,
2964
+ error: void 0,
2965
+ startedAt: void 0,
2966
+ completedAt: void 0
2967
+ });
2968
+ const nextChat = await tx.update(chats, chatId, { status: "queued" });
2969
+ const user = rows.find((row) => row?.data.role === "user" && (row.data.sequence ?? 0) < (assistant.data.sequence ?? 0));
2970
+ return acceptedFromTransaction(tx, nextChat, assistant.id, user?.id);
2971
+ });
2972
+ }
2973
+ async setTitle(id, title) {
2974
+ const update = mutateAtom(chats, (mutation) => mutation.update(id));
2975
+ return chatFromRow(await internalStore.set(internalJotaiWritableAtom(update), { title }));
2976
+ }
2977
+ async remove(id) {
2978
+ const transaction = transactionAtom(async (tx) => {
2979
+ const chat$1 = await tx.get(chats, id);
2980
+ if (!chat$1) return false;
2981
+ if (isActiveStatus(chat$1.data.status)) throw new Error("Active chats cannot be removed");
2982
+ const ids = await tx.query(chatMessages, (query) => query.where({ chatId: id }).ids());
2983
+ for (const messageId of ids) await tx.remove(chatMessages, messageId);
2984
+ return tx.remove(chats, id);
2985
+ });
2986
+ return internalStore.set(internalJotaiWritableAtom(transaction));
2987
+ }
2988
+ async recoverActive() {
2989
+ const transaction = transactionAtom(async (tx) => {
2990
+ const chatIds = await tx.query(chats, (query) => query.ids());
2991
+ for (const chatId of chatIds) {
2992
+ const chat$1 = await tx.get(chats, chatId);
2993
+ if (!chat$1) continue;
2994
+ const messageIds = await tx.query(chatMessages, (query) => query.where({ chatId }).ids());
2995
+ let recoveredMessage = false;
2996
+ for (const messageId of messageIds) {
2997
+ const message = await tx.get(chatMessages, messageId);
2998
+ if (message?.data.role === "assistant" && isActiveStatus(message.data.status ?? "")) {
2999
+ await tx.update(chatMessages, messageId, {
3000
+ status: "canceled",
3001
+ error: recoveryError(),
3002
+ completedAt: timestamp()
3003
+ });
3004
+ recoveredMessage = true;
3005
+ }
3006
+ }
3007
+ if (recoveredMessage || isActiveStatus(chat$1.data.status)) await tx.update(chats, chatId, { status: "canceled" });
3008
+ }
3009
+ });
3010
+ await internalStore.set(internalJotaiWritableAtom(transaction));
3011
+ }
3012
+ async requireChat(id) {
3013
+ const chat$1 = await this.get(id);
3014
+ if (!chat$1) throw new Error(`Chat not found: ${id}`);
3015
+ return chat$1;
3016
+ }
3017
+ async messageRow(id) {
3018
+ const row = queryAtom(chatMessages, (query) => query.row(id));
3019
+ return internalStore.get(internalJotaiAtom(row));
3020
+ }
3021
+ async transition(chatId, messageId, action) {
3022
+ const transaction = transactionAtom(async (tx) => {
3023
+ const chat$1 = await tx.get(chats, chatId);
3024
+ const message = await tx.get(chatMessages, messageId);
3025
+ if (!chat$1) throw new Error(`Chat not found: ${chatId}`);
3026
+ if (!message || message.data.chatId !== chatId) throw new Error(`Message not found: ${messageId}`);
3027
+ return action(tx, chat$1, message);
3028
+ });
3029
+ return internalStore.set(internalJotaiWritableAtom(transaction));
3030
+ }
3031
+ };
3032
+ const managerKey = Symbol.for("bleam.ai.chatManager.v1");
3033
+ function managedChats() {
3034
+ const scope = globalThis;
3035
+ scope[managerKey] ??= new ChatManager(new CollectionChatRepository(), { stream(options) {
3036
+ return streamChat(options);
3037
+ } });
3038
+ return scope[managerKey];
3039
+ }
3040
+ const chat = {
3041
+ create(input = {}) {
3042
+ return managedChats().create(input);
3043
+ },
3044
+ get(id) {
3045
+ return managedChats().get(id);
3046
+ },
3047
+ list() {
3048
+ return managedChats().list();
3049
+ },
3050
+ send(id, content, options) {
3051
+ return managedChats().send(id, content, options);
3052
+ },
3053
+ cancel(id) {
3054
+ return managedChats().cancel(id);
3055
+ },
3056
+ retry(id, messageId, options) {
3057
+ return managedChats().retry(id, messageId, options);
3058
+ },
3059
+ setTitle(id, title) {
3060
+ return managedChats().setTitle(id, title);
3061
+ },
3062
+ remove(id) {
3063
+ return managedChats().remove(id);
3064
+ }
3065
+ };
3066
+ function useChat(id, options = {}) {
3067
+ const manager = managedChats();
3068
+ use(manager.ready);
3069
+ const snapshot = useSyncExternalStore((listener) => manager.subscribeChat(id, listener), () => manager.getChatSnapshot(id));
3070
+ const generationOptions = optionsWithoutCallbacks(options);
3071
+ const callbacks = {
3072
+ onFinish: options.onFinish,
3073
+ onError: options.onError
3074
+ };
3075
+ const status = snapshot.chat?.status ?? "not-found";
3076
+ const active = status === "queued" || status === "generating" || status === "canceling";
3077
+ const error = snapshot.messages.findLast((message) => message.role === "assistant")?.error;
3078
+ return {
3079
+ chat: snapshot.chat,
3080
+ messages: snapshot.messages,
3081
+ status,
3082
+ error: error ?? null,
3083
+ isGenerating: active,
3084
+ canSend: snapshot.chat !== null && !active,
3085
+ canCancel: status === "queued" || status === "generating",
3086
+ send: (content, invocation) => manager.send(id, content, mergeOptions(generationOptions, invocation), callbacks),
3087
+ cancel: () => manager.cancel(id),
3088
+ retry: (messageId, invocation) => manager.retry(id, messageId, mergeOptions(generationOptions, invocation), callbacks),
3089
+ setTitle: (title) => manager.setTitle(id, title),
3090
+ remove: () => manager.remove(id)
3091
+ };
3092
+ }
3093
+ function useChatList() {
3094
+ const manager = managedChats();
3095
+ use(manager.ready);
3096
+ return useSyncExternalStore((listener) => manager.subscribeList(listener), () => manager.getListSnapshot());
3097
+ }
3098
+ async function acceptedFromTransaction(tx, chatRow, assistantId, userId) {
3099
+ const ids = await tx.query(chatMessages, (query) => query.where({ chatId: chatRow.id }).sortBy("sequence").ids());
3100
+ const messages = (await Promise.all(ids.map((id) => tx.get(chatMessages, id)))).flatMap((row) => row ? [messageFromRow(row)] : []);
3101
+ const assistantMessage = messages.find((message) => message.id === assistantId);
3102
+ if (assistantMessage?.role !== "assistant") throw new Error(`Assistant message not found: ${assistantId}`);
3103
+ const userMessage = userId ? messages.find((message) => message.id === userId) : void 0;
3104
+ return {
3105
+ chatId: chatRow.id,
3106
+ userMessageId: userMessage?.id ?? "",
3107
+ assistantMessageId: assistantId,
3108
+ chat: chatFromRow(chatRow),
3109
+ userMessage,
3110
+ assistantMessage,
3111
+ messages
3112
+ };
3113
+ }
3114
+ function chatFromRow(row) {
3115
+ if (!row) return null;
3116
+ return {
3117
+ id: row.id,
3118
+ title: row.data.title,
3119
+ status: row.data.status,
3120
+ createdAt: row.createdAt,
3121
+ updatedAt: row.updatedAt
3122
+ };
3123
+ }
3124
+ function messageFromRow(row) {
3125
+ return {
3126
+ id: row.id,
3127
+ chatId: row.data.chatId,
3128
+ role: row.data.role,
3129
+ status: row.data.status,
3130
+ content: row.data.content,
3131
+ attempt: row.data.attempt,
3132
+ sequence: row.data.sequence,
3133
+ error: row.data.error,
3134
+ createdAt: row.createdAt,
3135
+ updatedAt: row.updatedAt,
3136
+ startedAt: row.data.startedAt,
3137
+ completedAt: row.data.completedAt
3138
+ };
3139
+ }
3140
+ function optionsWithoutCallbacks(options) {
3141
+ const { onFinish: _onFinish, onError: _onError,...generation } = options;
3142
+ return generation;
3143
+ }
3144
+ function mergeOptions(defaults, invocation) {
3145
+ return {
3146
+ ...defaults,
3147
+ ...invocation
3148
+ };
3149
+ }
3150
+ function isActiveStatus(status) {
3151
+ return status === "queued" || status === "generating" || status === "canceling";
3152
+ }
3153
+ function recoveryError() {
3154
+ return {
3155
+ name: "AIError",
3156
+ code: "generation_canceled",
3157
+ message: "Generation was interrupted when the app exited"
3158
+ };
3159
+ }
3160
+ function timestamp() {
3161
+ return (/* @__PURE__ */ new Date()).toISOString();
3162
+ }
3163
+
3164
+ //#endregion
3165
+ //#region src/ai/image-batch.ts
3166
+ function batchImageSeeds(count, seed) {
3167
+ if (!Number.isInteger(count) || count < 1 || count > 64) throw new AIError("invalid_message", "Batch count must be an integer from 1 to 64");
3168
+ const first = seed ?? Math.floor(Math.random() * 4294967296);
3169
+ return Array.from({ length: count }, (_, index) => first + index >>> 0);
3170
+ }
3171
+
3172
+ //#endregion
3173
+ //#region src/ai/image-jobs.ts
3174
+ const jobShape = {
3175
+ status: string({ default: "paused" }),
3176
+ request: string({ default: "{}" }),
3177
+ progress: string({ default: "" }),
3178
+ result: string({ default: "" }),
3179
+ error: string({ default: "" }),
3180
+ attempt: number({ default: 1 }),
3181
+ startedAt: string({ default: "" }),
3182
+ completedAt: string({ default: "" })
3183
+ };
3184
+ var CollectionImageJobRepository = class {
3185
+ records = atomCollection("__bleam_ai_jobs", jobShape);
3186
+ insertRecord = mutateAtom(this.records, (mutation) => mutation.insert());
3187
+ async list() {
3188
+ const ids = await internalStore.get(internalJotaiAtom(this.records));
3189
+ return (await Promise.all(ids.map((id) => this.getRow(id)))).flatMap((row) => row ? [jobFromRow(row)] : []);
3190
+ }
3191
+ async insert(job) {
3192
+ const id = await internalStore.set(internalJotaiWritableAtom(this.insertRecord), dataFromJob(job));
3193
+ const row = await this.getRow(id);
3194
+ if (!row) throw new Error(`Could not load inserted image job ${id}`);
3195
+ return jobFromRow(row);
3196
+ }
3197
+ async update(job) {
3198
+ const updateRecord = mutateAtom(this.records, (mutation) => mutation.update(job.id));
3199
+ return jobFromRow(await internalStore.set(internalJotaiWritableAtom(updateRecord), dataFromJob(job)));
3200
+ }
3201
+ async remove(id) {
3202
+ const removeRecord = mutateAtom(this.records, (mutation) => mutation.remove(id));
3203
+ await internalStore.set(internalJotaiWritableAtom(removeRecord));
3204
+ }
3205
+ async getRow(id) {
3206
+ const row = queryAtom(this.records, (query) => query.row(id));
3207
+ return internalStore.get(internalJotaiAtom(row));
3208
+ }
3209
+ };
3210
+ var ImageJobManager = class {
3211
+ records = /* @__PURE__ */ new Map();
3212
+ jobListeners = /* @__PURE__ */ new Map();
3213
+ listListeners = /* @__PURE__ */ new Set();
3214
+ listSnapshot = [];
3215
+ operation = Promise.resolve();
3216
+ pumping = false;
3217
+ runningId;
3218
+ controller;
3219
+ ready;
3220
+ constructor(repository, executor, jobFiles = nativeImageJobFiles) {
3221
+ this.repository = repository;
3222
+ this.executor = executor;
3223
+ this.jobFiles = jobFiles;
3224
+ this.ready = this.initialize();
3225
+ }
3226
+ setExecutor(executor) {
3227
+ this.executor = executor;
3228
+ }
3229
+ async queue(options) {
3230
+ await this.ready;
3231
+ const request = normalizeRequest(options);
3232
+ const provisional = await this.repository.insert({
3233
+ kind: "image",
3234
+ status: "paused",
3235
+ request,
3236
+ attempt: 0
3237
+ });
3238
+ try {
3239
+ const preparedRequest = await this.jobFiles.prepare(provisional.id, request);
3240
+ const queued = await this.repository.update({
3241
+ ...provisional,
3242
+ status: "queued",
3243
+ request: preparedRequest,
3244
+ attempt: 1
3245
+ });
3246
+ this.set(queued);
3247
+ this.pump();
3248
+ return queued;
3249
+ } catch (error) {
3250
+ await this.repository.remove(provisional.id);
3251
+ await this.jobFiles.remove(provisional).catch(() => void 0);
3252
+ throw error;
3253
+ }
3254
+ }
3255
+ async get(id) {
3256
+ await this.ready;
3257
+ return this.records.get(id);
3258
+ }
3259
+ async list() {
3260
+ await this.ready;
3261
+ return this.listSnapshot;
3262
+ }
3263
+ async cancel(id) {
3264
+ await this.ready;
3265
+ return this.mutate(async () => {
3266
+ const job = this.records.get(id);
3267
+ if (!job) return false;
3268
+ if (job.status === "queued" || job.status === "paused") {
3269
+ await this.replace(job, {
3270
+ status: "canceled",
3271
+ error: canceledError(),
3272
+ completedAt: now()
3273
+ });
3274
+ return true;
3275
+ }
3276
+ if (job.status !== "running") return false;
3277
+ await this.replace(job, { status: "canceling" });
3278
+ if (this.runningId === id) this.controller?.abort();
3279
+ return true;
3280
+ });
3281
+ }
3282
+ async retry(id) {
3283
+ await this.ready;
3284
+ const resumed = await this.mutate(async () => {
3285
+ const job = this.records.get(id);
3286
+ if (!job || job.status !== "failed" && job.status !== "canceled") return false;
3287
+ await this.replace(job, {
3288
+ status: "queued",
3289
+ attempt: job.attempt + 1,
3290
+ progress: void 0,
3291
+ result: void 0,
3292
+ error: void 0,
3293
+ startedAt: void 0,
3294
+ completedAt: void 0
3295
+ });
3296
+ return true;
3297
+ });
3298
+ if (resumed) this.pump();
3299
+ return resumed;
3300
+ }
3301
+ async resume(id) {
3302
+ await this.ready;
3303
+ const resumed = await this.mutate(async () => {
3304
+ const job = this.records.get(id);
3305
+ if (job?.status !== "paused") return false;
3306
+ await this.replace(job, { status: "queued" });
3307
+ return true;
3308
+ });
3309
+ if (resumed) this.pump();
3310
+ return resumed;
3311
+ }
3312
+ async remove(id) {
3313
+ await this.ready;
3314
+ return this.mutate(async () => {
3315
+ const job = this.records.get(id);
3316
+ if (!job) return false;
3317
+ if (job.status === "queued" || job.status === "running" || job.status === "canceling") throw new AIError("concurrent_request", "Cancel the image job before removing it");
3318
+ await this.jobFiles.remove(job);
3319
+ await this.repository.remove(id);
3320
+ this.records.delete(id);
3321
+ this.refreshList();
3322
+ this.emit(id);
3323
+ return true;
3324
+ });
3325
+ }
3326
+ getJobSnapshot(id) {
3327
+ return this.records.get(id);
3328
+ }
3329
+ getListSnapshot() {
3330
+ return this.listSnapshot;
3331
+ }
3332
+ subscribeJob(id, listener) {
3333
+ const listeners = this.jobListeners.get(id) ?? /* @__PURE__ */ new Set();
3334
+ listeners.add(listener);
3335
+ this.jobListeners.set(id, listeners);
3336
+ return () => {
3337
+ listeners.delete(listener);
3338
+ if (listeners.size === 0) this.jobListeners.delete(id);
3339
+ };
3340
+ }
3341
+ subscribeList(listener) {
3342
+ this.listListeners.add(listener);
3343
+ return () => this.listListeners.delete(listener);
3344
+ }
3345
+ async initialize() {
3346
+ const jobs$1 = await this.repository.list();
3347
+ for (const job of jobs$1) {
3348
+ if (job.status === "paused" && job.attempt === 0) {
3349
+ await this.repository.remove(job.id);
3350
+ await this.jobFiles.remove(job).catch(() => void 0);
3351
+ continue;
3352
+ }
3353
+ let recovered = job;
3354
+ if (job.status === "running" || job.status === "canceling") recovered = await this.repository.update({
3355
+ ...job,
3356
+ status: "canceled",
3357
+ error: canceledError("Generation was interrupted when the app exited"),
3358
+ completedAt: now()
3359
+ });
3360
+ else if (job.status === "queued") recovered = await this.repository.update({
3361
+ ...job,
3362
+ status: "paused"
3363
+ });
3364
+ this.records.set(recovered.id, recovered);
3365
+ }
3366
+ this.refreshList();
3367
+ }
3368
+ mutate(action) {
3369
+ const result = this.operation.then(action, action);
3370
+ this.operation = result.then(() => void 0, () => void 0);
3371
+ return result;
3372
+ }
3373
+ pump() {
3374
+ if (this.pumping) return;
3375
+ this.pumping = true;
3376
+ this.runLoop().finally(() => {
3377
+ this.pumping = false;
3378
+ if (this.nextQueued()) this.pump();
3379
+ });
3380
+ }
3381
+ async runLoop() {
3382
+ while (true) {
3383
+ const job = await this.mutate(async () => {
3384
+ const next = this.nextQueued();
3385
+ if (!next) return void 0;
3386
+ this.runningId = next.id;
3387
+ this.controller = new AbortController();
3388
+ return this.replace(next, {
3389
+ status: "running",
3390
+ startedAt: now(),
3391
+ completedAt: void 0,
3392
+ error: void 0,
3393
+ progress: void 0
3394
+ });
3395
+ });
3396
+ if (!job) return;
3397
+ const controller = this.controller;
3398
+ try {
3399
+ const result = await this.executor({
3400
+ ...job.request,
3401
+ signal: controller.signal,
3402
+ onProgress: (progress) => {
3403
+ this.mutate(async () => {
3404
+ const current = this.records.get(job.id);
3405
+ if (current?.status === "running") await this.replace(current, { progress });
3406
+ });
3407
+ }
3408
+ });
3409
+ await this.mutate(async () => {
3410
+ const current = this.records.get(job.id);
3411
+ if (!current) return;
3412
+ if (controller.signal.aborted || current.status === "canceling") {
3413
+ await this.replace(current, {
3414
+ status: "canceled",
3415
+ error: canceledError(),
3416
+ completedAt: now()
3417
+ });
3418
+ return;
3419
+ }
3420
+ await this.replace(current, {
3421
+ status: "completed",
3422
+ progress: {
3423
+ currentStep: 1,
3424
+ totalSteps: 1,
3425
+ fraction: 1
3426
+ },
3427
+ result,
3428
+ completedAt: now()
3429
+ });
3430
+ });
3431
+ } catch (error) {
3432
+ await this.mutate(async () => {
3433
+ const current = this.records.get(job.id);
3434
+ if (!current) return;
3435
+ const canceled = controller.signal.aborted || current.status === "canceling";
3436
+ await this.replace(current, {
3437
+ status: canceled ? "canceled" : "failed",
3438
+ error: canceled ? canceledError() : serializedError(error),
3439
+ completedAt: now()
3440
+ });
3441
+ });
3442
+ } finally {
3443
+ this.runningId = void 0;
3444
+ this.controller = void 0;
3445
+ }
3446
+ }
3447
+ }
3448
+ nextQueued() {
3449
+ return [...this.records.values()].filter((job) => job.status === "queued").sort((left, right) => left.createdAt === right.createdAt ? left.id.localeCompare(right.id) : left.createdAt.localeCompare(right.createdAt))[0];
3450
+ }
3451
+ async replace(job, update) {
3452
+ const persisted = await this.repository.update({
3453
+ ...job,
3454
+ ...update
3455
+ });
3456
+ this.set(persisted);
3457
+ return persisted;
3458
+ }
3459
+ set(job) {
3460
+ this.records.set(job.id, job);
3461
+ this.refreshList();
3462
+ this.emit(job.id);
3463
+ }
3464
+ refreshList() {
3465
+ this.listSnapshot = [...this.records.values()].sort((left, right) => left.createdAt === right.createdAt ? right.id.localeCompare(left.id) : right.createdAt.localeCompare(left.createdAt)).map(summaryFromJob);
3466
+ for (const listener of this.listListeners) listener();
3467
+ }
3468
+ emit(id) {
3469
+ for (const listener of this.jobListeners.get(id) ?? []) listener();
3470
+ }
3471
+ };
3472
+ function useManagedJob(manager, id) {
3473
+ use(manager.ready);
3474
+ return useSyncExternalStore((listener) => manager.subscribeJob(id, listener), () => manager.getJobSnapshot(id));
3475
+ }
3476
+ function useManagedJobList(manager) {
3477
+ use(manager.ready);
3478
+ return useSyncExternalStore((listener) => manager.subscribeList(listener), () => manager.getListSnapshot());
3479
+ }
3480
+ const nativeImageJobFiles = {
3481
+ async prepare(id, request) {
3482
+ const inputs = jobRoot(id).directory("Inputs");
3483
+ await inputs.create();
3484
+ return {
3485
+ ...request,
3486
+ image: request.image ? await copyInput(request.image, inputs.path, "Image") : void 0,
3487
+ mask: request.mask ? await copyInput(request.mask, inputs.path, "Mask") : void 0,
3488
+ references: request.references ? await Promise.all(request.references.map((reference, index) => copyInput(reference, inputs.path, `Reference-${index + 1}`))) : void 0,
3489
+ loras: request.loras ? await Promise.all(request.loras.map(async (lora, index) => lora.source.type === "local" ? {
3490
+ ...lora,
3491
+ source: {
3492
+ ...lora.source,
3493
+ path: await copyInput(lora.source.path, inputs.path, `LoRA-${index + 1}`, ".safetensors")
3494
+ }
3495
+ } : lora)) : void 0
3496
+ };
3497
+ },
3498
+ async remove(job) {
3499
+ await jobRoot(job.id).delete().catch(() => void 0);
3500
+ if (job.result?.uri) await files.file(pathFromUri(job.result.uri)).delete().catch(() => void 0);
3501
+ }
3502
+ };
3503
+ function dataFromJob(job) {
3504
+ return {
3505
+ status: job.status,
3506
+ request: JSON.stringify(job.request),
3507
+ progress: encode(job.progress),
3508
+ result: encode(job.result),
3509
+ error: encode(job.error),
3510
+ attempt: job.attempt,
3511
+ startedAt: job.startedAt ?? "",
3512
+ completedAt: job.completedAt ?? ""
3513
+ };
3514
+ }
3515
+ function jobFromRow(row) {
3516
+ return {
3517
+ id: row.id,
3518
+ kind: "image",
3519
+ status: row.data.status,
3520
+ request: decode(row.data.request),
3521
+ progress: decode(row.data.progress),
3522
+ result: decode(row.data.result),
3523
+ error: decode(row.data.error),
3524
+ attempt: row.data.attempt,
3525
+ createdAt: row.createdAt,
3526
+ updatedAt: row.updatedAt,
3527
+ startedAt: row.data.startedAt || void 0,
3528
+ completedAt: row.data.completedAt || void 0
3529
+ };
3530
+ }
3531
+ function encode(value) {
3532
+ return value === void 0 ? "" : JSON.stringify(value);
3533
+ }
3534
+ function decode(value) {
3535
+ return value ? JSON.parse(value) : void 0;
3536
+ }
3537
+ function normalizeRequest(options) {
3538
+ const runtimeOptions = options;
3539
+ for (const key of [
3540
+ "accessToken",
3541
+ "onModelProgress",
3542
+ "onProgress",
3543
+ "signal",
3544
+ "tokenizer"
3545
+ ]) if (runtimeOptions[key] !== void 0) throw new AIError("invalid_message", `${key} is not supported for persistent image jobs`);
3546
+ if (!options.prompt.trim()) throw new AIError("invalid_message", "Expected prompt");
3547
+ assertSerializable(options);
3548
+ return {
3549
+ ...options,
3550
+ model: typeof options.model === "string" ? options.model : options.model?.id,
3551
+ seed: options.seed ?? Math.floor(Math.random() * 4294967296)
3552
+ };
3553
+ }
3554
+ function assertSerializable(value, path = "options") {
3555
+ if (typeof value === "number" && !Number.isFinite(value)) throw new AIError("invalid_message", `${path} must be finite`);
3556
+ if (typeof value === "function" || typeof value === "symbol" || typeof value === "bigint") throw new AIError("invalid_message", `${path} must be serializable`);
3557
+ if (Array.isArray(value)) value.forEach((next, index) => {
3558
+ assertSerializable(next, `${path}[${index}]`);
3559
+ });
3560
+ else if (value && typeof value === "object") for (const [key, next] of Object.entries(value)) assertSerializable(next, `${path}.${key}`);
3561
+ }
3562
+ function serializedError(error) {
3563
+ const value = error instanceof Error ? error : new Error(String(error));
3564
+ const record = value;
3565
+ return {
3566
+ name: value.name,
3567
+ message: value.message,
3568
+ code: typeof record.code === "string" ? record.code : void 0,
3569
+ stage: typeof record.stage === "string" ? record.stage : void 0
3570
+ };
3571
+ }
3572
+ function canceledError(message = "Image generation canceled") {
3573
+ return {
3574
+ name: "AIError",
3575
+ code: "generation_canceled",
3576
+ message
3577
+ };
3578
+ }
3579
+ function summaryFromJob(job) {
3580
+ return {
3581
+ id: job.id,
3582
+ kind: job.kind,
3583
+ status: job.status,
3584
+ prompt: job.request.prompt,
3585
+ progress: job.progress,
3586
+ attempt: job.attempt,
3587
+ createdAt: job.createdAt,
3588
+ updatedAt: job.updatedAt,
3589
+ startedAt: job.startedAt,
3590
+ completedAt: job.completedAt
3591
+ };
3592
+ }
3593
+ function jobRoot(id) {
3594
+ return files.directory(files.paths.shared, "ImageJobs", currentAppStorageKey(), id);
3595
+ }
3596
+ async function copyInput(source, directory, name, fallbackExtension = ".png") {
3597
+ const path = pathFromUri(source);
3598
+ const destination = `${directory}/${name}${path.match(/\.[A-Za-z0-9]+$/)?.[0] ?? fallbackExtension}`;
3599
+ await nativeCopy(path, destination);
3600
+ return destination;
3601
+ }
3602
+ function pathFromUri(value) {
3603
+ if (!value.startsWith("file://")) return value;
3604
+ try {
3605
+ return decodeURIComponent(new URL(value).pathname);
3606
+ } catch {
3607
+ return value.slice(7);
3608
+ }
3609
+ }
3610
+ function now() {
3611
+ return (/* @__PURE__ */ new Date()).toISOString();
3612
+ }
3613
+
3614
+ //#endregion
3615
+ //#region src/ai/image-models.ts
3616
+ const flux2Klein4B8BitAbliterated = {
3617
+ formatVersion: 1,
3618
+ model: "flux2-klein-4b-8bit-abliterated",
3619
+ directory: "flux2-klein-4b-8bit-abliterated",
3620
+ metadata: {
3621
+ displayName: "Flux.2 Klein 4B Abliterated",
3622
+ family: "flux2-klein",
3623
+ precision: "8-bit",
3624
+ estimatedMemoryGB: 12,
3625
+ quality: "high",
3626
+ speed: "fast",
3627
+ capabilities: [
3628
+ "text-to-image",
3629
+ "image-to-image",
3630
+ "inpainting",
3631
+ "outpainting",
3632
+ "references",
3633
+ "lora"
3634
+ ]
3635
+ },
3636
+ components: {
3637
+ textEncoder: {
3638
+ repo: "mlx-community/Josiefied-Qwen3-4B-abliterated-v1-8bit",
3639
+ revision: "main",
3640
+ path: "text-encoder",
3641
+ files: [
3642
+ "config.json",
3643
+ "tokenizer.json",
3644
+ "tokenizer_config.json",
3645
+ "*.safetensors",
3646
+ "*.safetensors.index.json"
3647
+ ]
3648
+ },
3649
+ transformer: {
3650
+ repo: "moxin-org/FLUX.2-klein-4B-8bit-mlx",
3651
+ revision: "main",
3652
+ path: "transformer",
3653
+ files: ["config.json", "quantized_8bit.safetensors"]
3654
+ },
3655
+ vae: {
3656
+ repo: "black-forest-labs/FLUX.2-klein-4B",
3657
+ revision: "main",
3658
+ path: "vae",
3659
+ files: [
3660
+ "config.json",
3661
+ "diffusion_pytorch_model.safetensors",
3662
+ "diffusion_pytorch_model.safetensors.index.json",
3663
+ "diffusion_pytorch_model-*.safetensors"
3664
+ ]
3665
+ }
3666
+ }
3667
+ };
3668
+ const ternaryBonsaiImage4B = {
3669
+ formatVersion: 1,
3670
+ model: "ternary-bonsai-image-4b",
3671
+ directory: "ternary-bonsai-image-4b",
3672
+ metadata: {
3673
+ displayName: "Ternary Bonsai Image 4B",
3674
+ family: "bonsai-image",
3675
+ precision: "2-bit",
3676
+ estimatedMemoryGB: 8,
3677
+ quality: "high",
3678
+ speed: "fast",
3679
+ capabilities: ["text-to-image"]
3680
+ },
3681
+ components: {
3682
+ textEncoder: {
3683
+ repo: "prism-ml/bonsai-image-ternary-4B-mlx-2bit",
3684
+ revision: "2c24c81b934a658ba5590cf39088ba929985b4a8",
3685
+ path: "text-encoder",
3686
+ files: [
3687
+ "text_encoder-mlx-4bit/config.json",
3688
+ "text_encoder-mlx-4bit/tokenizer.json",
3689
+ "text_encoder-mlx-4bit/tokenizer_config.json",
3690
+ "text_encoder-mlx-4bit/model.safetensors",
3691
+ "text_encoder-mlx-4bit/model.safetensors.index.json"
3692
+ ]
3693
+ },
3694
+ transformer: {
3695
+ repo: "prism-ml/bonsai-image-ternary-4B-mlx-2bit",
3696
+ revision: "2c24c81b934a658ba5590cf39088ba929985b4a8",
3697
+ path: "transformer",
3698
+ files: [
3699
+ "transformer-packed-mflux/config.json",
3700
+ "transformer-packed-mflux/diffusion_pytorch_model.safetensors",
3701
+ "transformer-packed-mflux/quantization_config.json"
3702
+ ]
3703
+ },
3704
+ vae: {
3705
+ repo: "prism-ml/bonsai-image-ternary-4B-mlx-2bit",
3706
+ revision: "2c24c81b934a658ba5590cf39088ba929985b4a8",
3707
+ path: "vae",
3708
+ files: ["vae/config.json", "vae/diffusion_pytorch_model.safetensors"]
3709
+ }
3710
+ }
3711
+ };
3712
+ const imageModelManifests = [flux2Klein4B8BitAbliterated, ternaryBonsaiImage4B];
3713
+ function imageModelManifest(model) {
3714
+ return imageModelManifests.find((manifest) => manifest.model === model);
3715
+ }
3716
+ const flux2KleinModelFiles = [
3717
+ {
3718
+ repo: flux2Klein4B8BitAbliterated.components.textEncoder.repo,
3719
+ revision: flux2Klein4B8BitAbliterated.components.textEncoder.revision,
3720
+ source: "config.json",
3721
+ destination: "text-encoder/config.json",
3722
+ bytes: 937
3723
+ },
3724
+ {
3725
+ repo: flux2Klein4B8BitAbliterated.components.textEncoder.repo,
3726
+ revision: flux2Klein4B8BitAbliterated.components.textEncoder.revision,
3727
+ source: "tokenizer.json",
3728
+ destination: "text-encoder/tokenizer.json",
3729
+ bytes: 11422654
3730
+ },
3731
+ {
3732
+ repo: flux2Klein4B8BitAbliterated.components.textEncoder.repo,
3733
+ revision: flux2Klein4B8BitAbliterated.components.textEncoder.revision,
3734
+ source: "tokenizer_config.json",
3735
+ destination: "text-encoder/tokenizer_config.json",
3736
+ bytes: 9706
3737
+ },
3738
+ {
3739
+ repo: flux2Klein4B8BitAbliterated.components.textEncoder.repo,
3740
+ revision: flux2Klein4B8BitAbliterated.components.textEncoder.revision,
3741
+ source: "model.safetensors",
3742
+ destination: "text-encoder/model.safetensors",
3743
+ bytes: 4274158989
3744
+ },
3745
+ {
3746
+ repo: flux2Klein4B8BitAbliterated.components.textEncoder.repo,
3747
+ revision: flux2Klein4B8BitAbliterated.components.textEncoder.revision,
3748
+ source: "model.safetensors.index.json",
3749
+ destination: "text-encoder/model.safetensors.index.json",
3750
+ bytes: 63924
3751
+ },
3752
+ {
3753
+ repo: flux2Klein4B8BitAbliterated.components.transformer.repo,
3754
+ revision: flux2Klein4B8BitAbliterated.components.transformer.revision,
3755
+ source: "transformer/config.json",
3756
+ destination: "transformer/config.json",
3757
+ bytes: 541
3758
+ },
3759
+ {
3760
+ repo: flux2Klein4B8BitAbliterated.components.transformer.repo,
3761
+ revision: flux2Klein4B8BitAbliterated.components.transformer.revision,
3762
+ source: "transformer/quantized_8bit.safetensors",
3763
+ destination: "transformer/quantized_8bit.safetensors",
3764
+ bytes: 4360050983
3765
+ },
3766
+ {
3767
+ repo: flux2Klein4B8BitAbliterated.components.vae.repo,
3768
+ revision: flux2Klein4B8BitAbliterated.components.vae.revision,
3769
+ source: "vae/config.json",
3770
+ destination: "vae/config.json",
3771
+ bytes: 821
3772
+ },
3773
+ {
3774
+ repo: flux2Klein4B8BitAbliterated.components.vae.repo,
3775
+ revision: flux2Klein4B8BitAbliterated.components.vae.revision,
3776
+ source: "vae/diffusion_pytorch_model.safetensors",
3777
+ destination: "vae/diffusion_pytorch_model.safetensors",
3778
+ bytes: 168120878
3779
+ }
3780
+ ];
3781
+ const flux2KleinTotalBytes = flux2KleinModelFiles.reduce((total, file) => total + file.bytes, 0);
3782
+ const bonsaiRepo = ternaryBonsaiImage4B.components.textEncoder.repo;
3783
+ const bonsaiRevision = ternaryBonsaiImage4B.components.textEncoder.revision;
3784
+ const ternaryBonsaiImageModelFiles = [
3785
+ {
3786
+ repo: bonsaiRepo,
3787
+ revision: bonsaiRevision,
3788
+ source: "text_encoder-mlx-4bit/config.json",
3789
+ destination: "text-encoder/config.json",
3790
+ bytes: 937
3791
+ },
3792
+ {
3793
+ repo: bonsaiRepo,
3794
+ revision: bonsaiRevision,
3795
+ source: "text_encoder-mlx-4bit/tokenizer.json",
3796
+ destination: "text-encoder/tokenizer.json",
3797
+ bytes: 11422654
3798
+ },
3799
+ {
3800
+ repo: bonsaiRepo,
3801
+ revision: bonsaiRevision,
3802
+ source: "text_encoder-mlx-4bit/tokenizer_config.json",
3803
+ destination: "text-encoder/tokenizer_config.json",
3804
+ bytes: 9706
3805
+ },
3806
+ {
3807
+ repo: bonsaiRepo,
3808
+ revision: bonsaiRevision,
3809
+ source: "text_encoder-mlx-4bit/model.safetensors",
3810
+ destination: "text-encoder/model.safetensors",
3811
+ bytes: 2263022529
3812
+ },
3813
+ {
3814
+ repo: bonsaiRepo,
3815
+ revision: bonsaiRevision,
3816
+ source: "text_encoder-mlx-4bit/model.safetensors.index.json",
3817
+ destination: "text-encoder/model.safetensors.index.json",
3818
+ bytes: 63924
3819
+ },
3820
+ {
3821
+ repo: bonsaiRepo,
3822
+ revision: bonsaiRevision,
3823
+ source: "transformer-packed-mflux/config.json",
3824
+ destination: "transformer/config.json",
3825
+ bytes: 619
3826
+ },
3827
+ {
3828
+ repo: bonsaiRepo,
3829
+ revision: bonsaiRevision,
3830
+ source: "transformer-packed-mflux/diffusion_pytorch_model.safetensors",
3831
+ destination: "transformer/diffusion_pytorch_model.safetensors",
3832
+ bytes: 1425271472
3833
+ },
3834
+ {
3835
+ repo: bonsaiRepo,
3836
+ revision: bonsaiRevision,
3837
+ source: "transformer-packed-mflux/quantization_config.json",
3838
+ destination: "transformer/quantization_config.json",
3839
+ bytes: 5054
3840
+ },
3841
+ {
3842
+ repo: bonsaiRepo,
3843
+ revision: bonsaiRevision,
3844
+ source: "vae/config.json",
3845
+ destination: "vae/config.json",
3846
+ bytes: 821
3847
+ },
3848
+ {
3849
+ repo: bonsaiRepo,
3850
+ revision: bonsaiRevision,
3851
+ source: "vae/diffusion_pytorch_model.safetensors",
3852
+ destination: "vae/diffusion_pytorch_model.safetensors",
3853
+ bytes: 168120878
3854
+ }
3855
+ ];
3856
+ const ternaryBonsaiImageTotalBytes = ternaryBonsaiImageModelFiles.reduce((total, file) => total + file.bytes, 0);
3857
+ function joinModelPath(root, component) {
3858
+ return `${root.replace(/\/+$/, "")}/${component.replace(/^\/+/, "")}`;
3859
+ }
3860
+ function fluxImageModelPaths(path, manifest = flux2Klein4B8BitAbliterated) {
3861
+ return {
3862
+ path,
3863
+ textEncoderPath: joinModelPath(path, manifest.components.textEncoder.path),
3864
+ transformerPath: joinModelPath(path, manifest.components.transformer.path),
3865
+ vaePath: joinModelPath(path, manifest.components.vae.path)
3866
+ };
3867
+ }
3868
+
3869
+ //#endregion
3870
+ //#region src/ai/image-schedule.ts
3871
+ function imageSigmaSchedule(schedules) {
3872
+ const configured = schedules.filter((schedule) => schedule !== void 0);
3873
+ if (configured.length === 0) return void 0;
3874
+ const first = configured[0];
3875
+ if (first.length === 0 || first.some((sigma, index) => !Number.isFinite(sigma) || sigma <= 0 || sigma > 1 || index > 0 && sigma >= first[index - 1])) throw new AIError("invalid_model", "LoRA sigmaSchedule must contain descending values greater than 0 and at most 1");
3876
+ if (configured.some((schedule) => schedule.length !== first.length || schedule.some((sigma, index) => sigma !== first[index]))) throw new AIError("invalid_model", "Selected LoRAs require conflicting sigma schedules");
3877
+ return [...first];
3878
+ }
3879
+
3880
+ //#endregion
3881
+ //#region src/ai/image-upscale.ts
3882
+ function normalizeUpscaleScale(value) {
3883
+ const scale = value ?? 2;
3884
+ if (!Number.isFinite(scale) || scale <= 1 || scale > 4) throw new AIError("invalid_model", "Upscale scale must be greater than 1 and at most 4");
3885
+ return scale;
3886
+ }
3887
+
2421
3888
  //#endregion
2422
3889
  //#region src/ai/image.ts
2423
- function modelCacheRoot(manifest = flux2Klein4B8BitAbliterated) {
3890
+ function imageModelId(model) {
3891
+ return typeof model === "string" ? model : model.id;
3892
+ }
3893
+ function imageModelDefinition(model) {
3894
+ const id = imageModelId(model);
3895
+ if (id === flux2Klein4B8BitAbliterated.model) return {
3896
+ manifest: flux2Klein4B8BitAbliterated,
3897
+ files: flux2KleinModelFiles,
3898
+ totalBytes: flux2KleinTotalBytes
3899
+ };
3900
+ if (id === ternaryBonsaiImage4B.model) return {
3901
+ manifest: ternaryBonsaiImage4B,
3902
+ files: ternaryBonsaiImageModelFiles,
3903
+ totalBytes: ternaryBonsaiImageTotalBytes
3904
+ };
3905
+ throw new AIError("invalid_model", `Unsupported image model: ${id}`);
3906
+ }
3907
+ function modelCacheRoot(manifest) {
2424
3908
  return files.directory(files.paths.models, manifest.directory);
2425
3909
  }
2426
3910
  function assertImageModel(model) {
2427
- if (model !== flux2Klein4B8BitAbliterated.model) throw new AIError("invalid_model", `Unsupported image model: ${model}`);
3911
+ imageModelDefinition(model);
2428
3912
  }
2429
- function modelRootFromOptions(modelRoot) {
2430
- return modelRoot ?? modelCacheRoot().path;
3913
+ function modelPathFromOptions(definition, path) {
3914
+ return path ?? modelCacheRoot(definition.manifest).path;
2431
3915
  }
2432
3916
  function downloadConcurrency(value) {
2433
3917
  if (value === void 0) return 2;
@@ -2594,11 +4078,11 @@ function cachedLoRAFile(lora) {
2594
4078
  return imageLoRARoot().directory(id).file(safeCacheName(sourceFileName(lora), "lora filename"));
2595
4079
  }
2596
4080
  async function ensureModelFile(input) {
2597
- const destination = files.file(input.modelRoot, input.file.destination);
4081
+ const destination = files.file(input.path, input.file.destination);
2598
4082
  const stat = await destination.stat();
2599
4083
  if (stat.type === "file" && stat.size === input.file.bytes) {
2600
4084
  input.onProgress?.({
2601
- model: flux2Klein4B8BitAbliterated.model,
4085
+ model: input.model,
2602
4086
  file: input.file.destination,
2603
4087
  index: input.index,
2604
4088
  total: input.total,
@@ -2615,7 +4099,7 @@ async function ensureModelFile(input) {
2615
4099
  resume: true,
2616
4100
  onProgress(progress) {
2617
4101
  input.onProgress?.({
2618
- model: flux2Klein4B8BitAbliterated.model,
4102
+ model: input.model,
2619
4103
  file: input.file.destination,
2620
4104
  index: input.index,
2621
4105
  total: input.total,
@@ -2624,16 +4108,16 @@ async function ensureModelFile(input) {
2624
4108
  }
2625
4109
  }).result;
2626
4110
  }
2627
- async function writePreparedManifest(modelRoot) {
2628
- await files.file(modelRoot, "manifest.json").write(JSON.stringify(flux2Klein4B8BitAbliterated, null, 2));
4111
+ async function writePreparedManifest(path, manifest) {
4112
+ await files.file(path, "manifest.json").write(JSON.stringify(manifest, null, 2));
2629
4113
  }
2630
- async function imageModelState(model, modelRoot) {
2631
- assertImageModel(model);
4114
+ async function imageModelState(model, path) {
4115
+ const definition = imageModelDefinition(model);
2632
4116
  let bytesPresent = 0;
2633
4117
  const missingFiles = [];
2634
4118
  const invalidFiles = [];
2635
- for (const file of flux2KleinModelFiles) {
2636
- const stat = await files.file(modelRoot, file.destination).stat();
4119
+ for (const file of definition.files) {
4120
+ const stat = await files.file(path, file.destination).stat();
2637
4121
  if (stat.type !== "file") {
2638
4122
  missingFiles.push(file.destination);
2639
4123
  continue;
@@ -2641,17 +4125,17 @@ async function imageModelState(model, modelRoot) {
2641
4125
  bytesPresent += Math.min(stat.size ?? 0, file.bytes);
2642
4126
  if (stat.size !== file.bytes) invalidFiles.push(file.destination);
2643
4127
  }
2644
- const installed = missingFiles.length === 0;
2645
- const manifestStat = await files.file(modelRoot, "manifest.json").stat();
4128
+ const manifestStat = await files.file(path, "manifest.json").stat();
2646
4129
  if (manifestStat.type !== "file") missingFiles.push("manifest.json");
4130
+ const installed = missingFiles.length === 0;
2647
4131
  return {
2648
4132
  model,
2649
- modelRoot,
2650
- paths: fluxImageModelPaths(modelRoot),
4133
+ path,
4134
+ paths: fluxImageModelPaths(path, definition.manifest),
2651
4135
  installed,
2652
4136
  verified: installed && invalidFiles.length === 0 && manifestStat.type === "file",
2653
4137
  bytesPresent,
2654
- totalBytes: flux2KleinTotalBytes,
4138
+ totalBytes: definition.totalBytes,
2655
4139
  missingFiles,
2656
4140
  invalidFiles
2657
4141
  };
@@ -2661,13 +4145,13 @@ function tokenArray(value, name) {
2661
4145
  const values = Array.isArray(candidate) ? candidate : ArrayBuffer.isView(candidate) ? Array.from(candidate) : void 0;
2662
4146
  if (!values) throw new AIError("invalid_message", `Tokenizer did not return ${name}`);
2663
4147
  return values.map((token) => {
2664
- const number = typeof token === "bigint" ? Number(token) : token;
2665
- if (!Number.isInteger(number) || number < 0) throw new AIError("invalid_message", `${name} must contain token ids`);
2666
- return number;
4148
+ const number$1 = typeof token === "bigint" ? Number(token) : token;
4149
+ if (!Number.isInteger(number$1) || number$1 < 0) throw new AIError("invalid_message", `${name} must contain token ids`);
4150
+ return number$1;
2667
4151
  });
2668
4152
  }
2669
- async function createFluxTokenizer(modelRoot) {
2670
- const paths = fluxImageModelPaths(modelRoot);
4153
+ async function createFluxTokenizer(path, manifest) {
4154
+ const paths = fluxImageModelPaths(path, manifest);
2671
4155
  const tokenizer = new Tokenizer_default(await files.file(paths.textEncoderPath, "tokenizer.json").json(), await files.file(paths.textEncoderPath, "tokenizer_config.json").json());
2672
4156
  const padTokenId = 151643;
2673
4157
  return ({ prompt, maxLength }) => {
@@ -2714,18 +4198,18 @@ function assertTokenArray(value, name) {
2714
4198
  if (value.length === 0) throw new AIError("invalid_message", `${name} must not be empty`);
2715
4199
  for (const token of value) if (!Number.isInteger(token) || token < 0) throw new AIError("invalid_message", `${name} must contain token ids`);
2716
4200
  }
2717
- function imageResult(value, fallbackMetadata) {
2718
- 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");
4201
+ function imageResult(value, model, fallbackMetadata) {
4202
+ 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");
2719
4203
  const result = value;
2720
4204
  const metadata = isRecord(result.metadata) ? result.metadata : {};
2721
4205
  return {
2722
4206
  uri: value.uri,
2723
4207
  width: value.width,
2724
4208
  height: value.height,
2725
- model: value.model,
4209
+ model,
2726
4210
  metadata: {
2727
4211
  ...fallbackMetadata,
2728
- model: metadata.model === value.model ? metadata.model : fallbackMetadata.model,
4212
+ model: metadata.model === model ? model : fallbackMetadata.model,
2729
4213
  width: typeof metadata.width === "number" ? metadata.width : value.width,
2730
4214
  height: typeof metadata.height === "number" ? metadata.height : value.height,
2731
4215
  steps: typeof metadata.steps === "number" ? metadata.steps : fallbackMetadata.steps,
@@ -2774,15 +4258,18 @@ function nativeErrorMessage(error) {
2774
4258
  return (error instanceof Error ? error.message : String(error)).replace(/^\[(download|tokenize|encode|denoise|decode|write)]\s*/, "");
2775
4259
  }
2776
4260
  async function prepareImageModel(options = {}) {
2777
- const model = options.model ?? flux2Klein4B8BitAbliterated.model;
2778
- assertImageModel(model);
2779
- const root = modelCacheRoot();
4261
+ const definition = imageModelDefinition(options.model ?? flux2Klein4B8BitAbliterated.model);
4262
+ return prepareImageModelAtPath(definition, options, options.directory ? files.directory(options.directory, definition.manifest.directory).path : modelCacheRoot(definition.manifest).path);
4263
+ }
4264
+ async function prepareImageModelAtPath(definition, options, path) {
4265
+ const model = definition.manifest.model;
4266
+ const root = files.directory(path);
2780
4267
  await root.create({ recursive: true });
2781
4268
  const headers = options.accessToken ? { Authorization: `Bearer ${options.accessToken}` } : void 0;
2782
- const total = flux2KleinModelFiles.length;
4269
+ const total = definition.files.length;
2783
4270
  const bytesByFile = /* @__PURE__ */ new Map();
2784
4271
  const emitProgress = (progress) => {
2785
- const file = flux2KleinModelFiles.find((candidate) => candidate.destination === progress.file);
4272
+ const file = definition.files.find((candidate) => candidate.destination === progress.file);
2786
4273
  const bytesWritten = file ? Math.min(progress.bytesWritten, file.bytes) : progress.bytesWritten;
2787
4274
  bytesByFile.set(progress.file, bytesWritten);
2788
4275
  const modelBytesWritten = Array.from(bytesByFile.values()).reduce((sum, bytes) => sum + bytes, 0);
@@ -2790,12 +4277,12 @@ async function prepareImageModel(options = {}) {
2790
4277
  ...progress,
2791
4278
  bytesWritten,
2792
4279
  modelBytesWritten,
2793
- totalModelBytes: flux2KleinTotalBytes,
2794
- modelFraction: modelBytesWritten / flux2KleinTotalBytes
4280
+ totalModelBytes: definition.totalBytes,
4281
+ modelFraction: modelBytesWritten / definition.totalBytes
2795
4282
  });
2796
4283
  };
2797
4284
  const missingFiles = [];
2798
- for (const [index, file] of flux2KleinModelFiles.entries()) {
4285
+ for (const [index, file] of definition.files.entries()) {
2799
4286
  const stat = await files.file(root.path, file.destination).stat();
2800
4287
  if (stat.type === "file" && stat.size === file.bytes) emitProgress({
2801
4288
  model,
@@ -2813,7 +4300,8 @@ async function prepareImageModel(options = {}) {
2813
4300
  }
2814
4301
  await runConcurrent(missingFiles, downloadConcurrency(options.concurrency), async ({ file, index }) => {
2815
4302
  await ensureModelFile({
2816
- modelRoot: root.path,
4303
+ model,
4304
+ path: root.path,
2817
4305
  file,
2818
4306
  headers,
2819
4307
  index,
@@ -2821,22 +4309,26 @@ async function prepareImageModel(options = {}) {
2821
4309
  onProgress: emitProgress
2822
4310
  });
2823
4311
  });
2824
- await writePreparedManifest(root.path);
4312
+ await writePreparedManifest(root.path, definition.manifest);
2825
4313
  return {
2826
4314
  model,
2827
- ...fluxImageModelPaths(root.path),
2828
- tokenizer: await createFluxTokenizer(root.path)
4315
+ ...fluxImageModelPaths(root.path, definition.manifest),
4316
+ tokenizer: await createFluxTokenizer(root.path, definition.manifest)
2829
4317
  };
2830
4318
  }
2831
4319
  async function verifyImageModel(options = {}) {
2832
- return imageModelState(options.model ?? flux2Klein4B8BitAbliterated.model, modelRootFromOptions(options.modelRoot));
4320
+ const definition = imageModelDefinition(options.model ?? flux2Klein4B8BitAbliterated.model);
4321
+ return imageModelState(definition.manifest.model, modelPathFromOptions(definition, options.path));
2833
4322
  }
2834
4323
  async function listImageModels(options = {}) {
2835
- return [await imageModelState(flux2Klein4B8BitAbliterated.model, modelRootFromOptions(options.modelRoot))];
4324
+ return [...await Promise.all([flux2Klein4B8BitAbliterated.model, ternaryBonsaiImage4B.model].map((model) => {
4325
+ const definition = imageModelDefinition(model);
4326
+ return imageModelState(model, options.directory ? files.directory(options.directory, definition.manifest.directory).path : modelPathFromOptions(definition, void 0));
4327
+ }))];
2836
4328
  }
2837
4329
  async function deleteImageModel(options = {}) {
2838
- assertImageModel(options.model ?? flux2Klein4B8BitAbliterated.model);
2839
- await files.directory(modelRootFromOptions(options.modelRoot)).delete({ recursive: true });
4330
+ const definition = imageModelDefinition(options.model ?? flux2Klein4B8BitAbliterated.model);
4331
+ await files.directory(modelPathFromOptions(definition, options.path)).delete({ recursive: true });
2840
4332
  }
2841
4333
  async function imageMemoryTelemetry() {
2842
4334
  const value = await bridge().imageMemoryTelemetry?.();
@@ -2974,8 +4466,8 @@ async function prepareSelectedImageLoRAs(loras, model, options = {}) {
2974
4466
  }
2975
4467
  async function generateImage(options) {
2976
4468
  throwIfAborted(options.signal);
2977
- const model = options.model ?? flux2Klein4B8BitAbliterated.model;
2978
- assertImageModel(model);
4469
+ const definition = imageModelDefinition(options.model ?? flux2Klein4B8BitAbliterated.model);
4470
+ const model = definition.manifest.model;
2979
4471
  const prompt = options.prompt.trim();
2980
4472
  if (!prompt) throw new AIError("invalid_message", "Expected prompt");
2981
4473
  const presetSize = options.aspectRatio ? imageSizeForPreset(options.aspectRatio) : void 0;
@@ -3034,16 +4526,16 @@ async function generateImage(options) {
3034
4526
  const sigmaSchedule = imageSigmaSchedule(selectedLoRAs.map((lora) => lora.metadata?.sigmaSchedule));
3035
4527
  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})`);
3036
4528
  const generationSteps = sigmaSchedule?.length ?? steps;
3037
- const prepared = options.modelRoot && options.tokenizer ? void 0 : await withImageStage("download", () => prepareImageModel({
4529
+ const prepared = options.path && options.tokenizer ? void 0 : await withImageStage("download", () => prepareImageModelAtPath(definition, {
3038
4530
  model,
3039
4531
  accessToken: options.accessToken,
3040
4532
  concurrency: options.concurrency,
3041
4533
  onProgress: options.onModelProgress
3042
- }));
4534
+ }, options.path ?? modelCacheRoot(definition.manifest).path));
3043
4535
  throwIfAborted(options.signal);
3044
4536
  const tokenizer = options.tokenizer ?? prepared?.tokenizer;
3045
- const modelRoot = options.modelRoot ?? prepared?.modelRoot;
3046
- if (!tokenizer || !modelRoot) throw new AIError("invalid_model", "Expected image model root and tokenizer");
4537
+ const modelPath = options.path ?? prepared?.path;
4538
+ if (!tokenizer || !modelPath) throw new AIError("invalid_model", "Expected image model path and tokenizer");
3047
4539
  const tokenized = await withImageStage("tokenize", () => tokenizer({
3048
4540
  prompt,
3049
4541
  maxLength: 512
@@ -3054,7 +4546,7 @@ async function generateImage(options) {
3054
4546
  assertTokenArray(tokenized.attentionMask, "attentionMask");
3055
4547
  if (tokenized.inputIds.length !== tokenized.attentionMask.length) throw new AIError("invalid_message", "inputIds and attentionMask must have the same length");
3056
4548
  });
3057
- const paths = fluxImageModelPaths(modelRoot);
4549
+ const paths = fluxImageModelPaths(modelPath, definition.manifest);
3058
4550
  const request = {
3059
4551
  model,
3060
4552
  prompt,
@@ -3084,7 +4576,10 @@ async function generateImage(options) {
3084
4576
  seed,
3085
4577
  inputIds: tokenized.inputIds,
3086
4578
  attentionMask: tokenized.attentionMask,
3087
- ...paths
4579
+ modelRoot: paths.path,
4580
+ textEncoderPath: paths.textEncoderPath,
4581
+ transformerPath: paths.transformerPath,
4582
+ vaePath: paths.vaePath
3088
4583
  };
3089
4584
  const fallbackMetadata = {
3090
4585
  model,
@@ -3115,7 +4610,7 @@ async function generateImage(options) {
3115
4610
  throwIfAborted(options.signal);
3116
4611
  const value = await native.generateImage?.(request);
3117
4612
  throwIfAborted(options.signal);
3118
- return imageResult(value, fallbackMetadata);
4613
+ return imageResult(value, model, fallbackMetadata);
3119
4614
  } finally {
3120
4615
  subscription?.remove();
3121
4616
  options.signal?.removeEventListener("abort", cancelNative);
@@ -3141,22 +4636,36 @@ async function generateImages(options) {
3141
4636
  }
3142
4637
  return results;
3143
4638
  }
3144
- const imageGenerationQueueKey = Symbol.for("bleam.ai.imageGenerationQueue");
3145
- function imageGenerationQueue() {
4639
+ const imageJobManagerKey = Symbol.for("bleam.ai.imageJobManager.v2");
4640
+ function imageJobManager() {
3146
4641
  const scope = globalThis;
3147
- const queue = scope[imageGenerationQueueKey] ?? new ImageGenerationQueue(generateImage);
3148
- queue.setExecutor(generateImage);
3149
- scope[imageGenerationQueueKey] = queue;
3150
- return queue;
4642
+ const manager = scope[imageJobManagerKey] ?? new ImageJobManager(new CollectionImageJobRepository(), generateImage);
4643
+ manager.setExecutor(generateImage);
4644
+ scope[imageJobManagerKey] = manager;
4645
+ return manager;
3151
4646
  }
3152
- function scheduleImageGeneration(options) {
3153
- return imageGenerationQueue().schedule(options);
4647
+ async function queueImage(options) {
4648
+ return imageJobManager().queue(options);
3154
4649
  }
3155
- function getImageGenerationJob(id) {
3156
- return imageGenerationQueue().get(id);
4650
+ const image = Object.freeze({
4651
+ generate: generateImage,
4652
+ generateMany: generateImages,
4653
+ queue: queueImage,
4654
+ upscale: upscaleImage
4655
+ });
4656
+ const jobs = Object.freeze({
4657
+ get: (id) => imageJobManager().get(id),
4658
+ list: () => imageJobManager().list(),
4659
+ cancel: (id) => imageJobManager().cancel(id),
4660
+ retry: (id) => imageJobManager().retry(id),
4661
+ resume: (id) => imageJobManager().resume(id),
4662
+ remove: (id) => imageJobManager().remove(id)
4663
+ });
4664
+ function useJob(id) {
4665
+ return useManagedJob(imageJobManager(), id);
3157
4666
  }
3158
- function listImageGenerationJobs() {
3159
- return imageGenerationQueue().list();
4667
+ function useJobList() {
4668
+ return useManagedJobList(imageJobManager());
3160
4669
  }
3161
4670
 
3162
4671
  //#endregion
@@ -3226,7 +4735,7 @@ function buildImageGenerationRequest(input, options = {}) {
3226
4735
  ...options
3227
4736
  };
3228
4737
  if (input.mode === "text") {
3229
- const references = (input.refs ?? []).map((image) => image.path);
4738
+ const references = (input.refs ?? []).map((image$1) => image$1.path);
3230
4739
  return {
3231
4740
  ...base,
3232
4741
  references: references.length > 0 ? references : void 0,
@@ -3239,7 +4748,7 @@ function buildImageGenerationRequest(input, options = {}) {
3239
4748
  if (input.mode === "subject") {
3240
4749
  if (!input.subject) throw new Error("Subject mode requires a subject image");
3241
4750
  const refs = input.refs ?? [];
3242
- const references = [input.subject, ...refs].map((image) => image.path);
4751
+ const references = [input.subject, ...refs].map((image$1) => image$1.path);
3243
4752
  const size = imageSizeForAspectRatio(input.subject.size);
3244
4753
  return {
3245
4754
  ...base,
@@ -3252,7 +4761,7 @@ function buildImageGenerationRequest(input, options = {}) {
3252
4761
  }
3253
4762
  if (input.mode === "edit") {
3254
4763
  if (!input.source) throw new Error("Edit requires a source image");
3255
- const references = (input.refs ?? []).map((image) => image.path);
4764
+ const references = (input.refs ?? []).map((image$1) => image$1.path);
3256
4765
  const size = roundedImageSize(input.source.size);
3257
4766
  return {
3258
4767
  ...base,
@@ -3280,823 +4789,6 @@ function buildImageGenerationRequest(input, options = {}) {
3280
4789
  height: defaultImageSize
3281
4790
  };
3282
4791
  }
3283
- function useImageComposer(modeOrOptions, options = {}) {
3284
- if (typeof modeOrOptions !== "string") return useImageComposerController(modeOrOptions ?? {});
3285
- const mode = modeOrOptions;
3286
- if (mode === "text") return useTextComposer(options);
3287
- if (mode === "subject") return useSubjectComposer(options);
3288
- if (mode === "edit") return useEditComposer(options);
3289
- return useOutpaintComposer(options);
3290
- }
3291
- function useImageComposerController(options) {
3292
- const [mode, setMode] = useAtom(useComposerAtom(options.initialMode ?? "text"));
3293
- const text = useTextComposer(options);
3294
- const subject = useSubjectComposer(options);
3295
- const edit = useEditComposer(options);
3296
- const outpaint = useOutpaintComposer(options);
3297
- const active = mode === "text" ? text : mode === "subject" ? subject : mode === "edit" ? edit : outpaint;
3298
- const error = active.error ?? text.error ?? subject.error ?? edit.error ?? outpaint.error;
3299
- return {
3300
- mode,
3301
- setMode,
3302
- active,
3303
- text,
3304
- subject,
3305
- edit,
3306
- outpaint,
3307
- isPicking: text.isPicking || subject.isPicking || edit.isPicking || outpaint.isPicking,
3308
- error,
3309
- request: (prompt) => active.request(prompt),
3310
- clear() {
3311
- text.clear();
3312
- subject.clear();
3313
- edit.clear();
3314
- outpaint.clear();
3315
- }
3316
- };
3317
- }
3318
- function useTextComposer(options) {
3319
- const store = useComposerAtom({
3320
- refs: [],
3321
- isPicking: false,
3322
- error: null
3323
- });
3324
- const [state, setState] = useAtom(store);
3325
- const maxRefs = options.maxRefs ?? 3;
3326
- async function pickRefs() {
3327
- setState((current) => ({
3328
- ...current,
3329
- isPicking: true,
3330
- error: null
3331
- }));
3332
- try {
3333
- const refs = [...store.get().refs, ...await pickImages(options.inputRoot)].slice(0, maxRefs);
3334
- setState((current) => ({
3335
- ...current,
3336
- refs,
3337
- isPicking: false
3338
- }));
3339
- } catch (error) {
3340
- setState((current) => ({
3341
- ...current,
3342
- error: errorFromUnknown(error),
3343
- isPicking: false
3344
- }));
3345
- }
3346
- }
3347
- return {
3348
- mode: "text",
3349
- ...state,
3350
- pickRefs,
3351
- removeRef: (index) => setState((current) => removeRefFromState(current, index)),
3352
- request: (prompt) => ({
3353
- mode: "text",
3354
- prompt,
3355
- refs: store.get().refs
3356
- }),
3357
- clear: () => setState({
3358
- refs: [],
3359
- isPicking: false,
3360
- error: null
3361
- })
3362
- };
3363
- }
3364
- function useSubjectComposer(options) {
3365
- const store = useComposerAtom({
3366
- subject: null,
3367
- refs: [],
3368
- isPicking: false,
3369
- error: null
3370
- });
3371
- const [state, setState] = useAtom(store);
3372
- const maxRefs = options.maxRefs ?? 2;
3373
- async function pickSubject() {
3374
- setState((current) => ({
3375
- ...current,
3376
- isPicking: true,
3377
- error: null
3378
- }));
3379
- try {
3380
- const subject = await pickOneImage(options.inputRoot);
3381
- setState((current) => ({
3382
- ...current,
3383
- subject: subject ?? current.subject,
3384
- isPicking: false
3385
- }));
3386
- } catch (error) {
3387
- setState((current) => ({
3388
- ...current,
3389
- error: errorFromUnknown(error),
3390
- isPicking: false
3391
- }));
3392
- }
3393
- }
3394
- async function pickRefs() {
3395
- setState((current) => ({
3396
- ...current,
3397
- isPicking: true,
3398
- error: null
3399
- }));
3400
- try {
3401
- const refs = [...store.get().refs, ...await pickImages(options.inputRoot)].slice(0, maxRefs);
3402
- setState((current) => ({
3403
- ...current,
3404
- refs,
3405
- isPicking: false
3406
- }));
3407
- } catch (error) {
3408
- setState((current) => ({
3409
- ...current,
3410
- error: errorFromUnknown(error),
3411
- isPicking: false
3412
- }));
3413
- }
3414
- }
3415
- return {
3416
- mode: "subject",
3417
- ...state,
3418
- pickSubject,
3419
- removeSubject: () => setState((current) => ({
3420
- ...current,
3421
- subject: null
3422
- })),
3423
- pickRefs,
3424
- removeRef: (index) => setState((current) => removeRefFromState(current, index)),
3425
- request: (prompt) => {
3426
- const current = store.get();
3427
- return {
3428
- mode: "subject",
3429
- prompt,
3430
- subject: current.subject,
3431
- refs: current.refs
3432
- };
3433
- },
3434
- clear: () => setState({
3435
- subject: null,
3436
- refs: [],
3437
- isPicking: false,
3438
- error: null
3439
- })
3440
- };
3441
- }
3442
- function useEditComposer(options) {
3443
- const store = useComposerAtom({
3444
- source: null,
3445
- refs: [],
3446
- isPicking: false,
3447
- error: null
3448
- });
3449
- const [state, setState] = useAtom(store);
3450
- const maxRefs = options.maxRefs ?? 3;
3451
- async function pickSource() {
3452
- setState((current) => ({
3453
- ...current,
3454
- isPicking: true,
3455
- error: null
3456
- }));
3457
- try {
3458
- const source = await pickOneImage(options.inputRoot);
3459
- setState((current) => ({
3460
- ...current,
3461
- source: source ?? current.source,
3462
- isPicking: false
3463
- }));
3464
- } catch (error) {
3465
- setState((current) => ({
3466
- ...current,
3467
- error: errorFromUnknown(error),
3468
- isPicking: false
3469
- }));
3470
- }
3471
- }
3472
- async function pickRefs() {
3473
- setState((current) => ({
3474
- ...current,
3475
- isPicking: true,
3476
- error: null
3477
- }));
3478
- try {
3479
- const refs = [...store.get().refs, ...await pickImages(options.inputRoot)].slice(0, maxRefs);
3480
- setState((current) => ({
3481
- ...current,
3482
- refs,
3483
- isPicking: false
3484
- }));
3485
- } catch (error) {
3486
- setState((current) => ({
3487
- ...current,
3488
- error: errorFromUnknown(error),
3489
- isPicking: false
3490
- }));
3491
- }
3492
- }
3493
- return {
3494
- mode: "edit",
3495
- ...state,
3496
- pickSource,
3497
- removeSource: () => setState((current) => ({
3498
- ...current,
3499
- source: null
3500
- })),
3501
- pickRefs,
3502
- removeRef: (index) => setState((current) => removeRefFromState(current, index)),
3503
- request: (prompt) => {
3504
- const current = store.get();
3505
- return {
3506
- mode: "edit",
3507
- prompt,
3508
- source: current.source,
3509
- refs: current.refs
3510
- };
3511
- },
3512
- clear: () => setState({
3513
- source: null,
3514
- refs: [],
3515
- isPicking: false,
3516
- error: null
3517
- })
3518
- };
3519
- }
3520
- function useOutpaintComposer(options) {
3521
- const store = useComposerAtom({
3522
- source: null,
3523
- rect: null,
3524
- isPicking: false,
3525
- error: null
3526
- });
3527
- const [state, setState] = useAtom(store);
3528
- async function pickSource() {
3529
- setState((current) => ({
3530
- ...current,
3531
- isPicking: true,
3532
- error: null
3533
- }));
3534
- try {
3535
- const source = await pickOneImage(options.inputRoot);
3536
- setState((current) => ({
3537
- ...current,
3538
- source: source ?? current.source,
3539
- rect: source ? initialOutpaintRect(source.size) : current.rect,
3540
- isPicking: false
3541
- }));
3542
- } catch (error) {
3543
- setState((current) => ({
3544
- ...current,
3545
- error: errorFromUnknown(error),
3546
- isPicking: false
3547
- }));
3548
- }
3549
- }
3550
- return {
3551
- mode: "outpaint",
3552
- ...state,
3553
- pickSource,
3554
- removeSource: () => setState((current) => ({
3555
- ...current,
3556
- source: null,
3557
- rect: null
3558
- })),
3559
- setRect: (rect) => setState((current) => ({
3560
- ...current,
3561
- rect
3562
- })),
3563
- request: (prompt) => {
3564
- const current = store.get();
3565
- return {
3566
- mode: "outpaint",
3567
- prompt,
3568
- source: current.source,
3569
- rect: current.rect
3570
- };
3571
- },
3572
- clear: () => setState({
3573
- source: null,
3574
- rect: null,
3575
- isPicking: false,
3576
- error: null
3577
- })
3578
- };
3579
- }
3580
- function useImageGeneration() {
3581
- const [state, setState] = useAtom(useComposerAtom({
3582
- imageUri: null,
3583
- result: null,
3584
- progress: 0,
3585
- status: "",
3586
- isGenerating: false,
3587
- error: null
3588
- }));
3589
- const abortControllerRef = useStableRef(null);
3590
- async function run(input, options = {}) {
3591
- if (abortControllerRef.current) return null;
3592
- const abortController = new AbortController();
3593
- abortControllerRef.current = abortController;
3594
- setState((current) => ({
3595
- ...current,
3596
- imageUri: null,
3597
- result: null,
3598
- progress: 0,
3599
- status: "Generating...",
3600
- isGenerating: true,
3601
- error: null
3602
- }));
3603
- try {
3604
- const result = await generateImage(buildImageGenerationRequest(input, {
3605
- ...options,
3606
- signal: abortController.signal,
3607
- onModelProgress(progress) {
3608
- setState((current) => ({
3609
- ...current,
3610
- progress: progress.modelFraction >= 1 ? .15 : progress.modelFraction * .15,
3611
- status: progress.modelFraction >= 1 ? "Generating..." : modelProgressStatus(progress)
3612
- }));
3613
- },
3614
- onProgress(progress) {
3615
- setState((current) => ({
3616
- ...current,
3617
- progress: .15 + progress.currentStep / progress.totalSteps * .85,
3618
- status: `Step ${progress.currentStep}/${progress.totalSteps}`
3619
- }));
3620
- }
3621
- }));
3622
- setState((current) => ({
3623
- ...current,
3624
- imageUri: result.uri,
3625
- result,
3626
- progress: 1,
3627
- status: `${result.metadata.width}×${result.metadata.height} • ${result.metadata.steps} steps`,
3628
- isGenerating: false
3629
- }));
3630
- return result;
3631
- } catch (error) {
3632
- setState((current) => ({
3633
- ...current,
3634
- status: error instanceof Error ? error.message : String(error),
3635
- error: errorFromUnknown(error),
3636
- isGenerating: false
3637
- }));
3638
- return null;
3639
- } finally {
3640
- if (abortControllerRef.current === abortController) abortControllerRef.current = null;
3641
- }
3642
- }
3643
- function cancel() {
3644
- setState((current) => ({
3645
- ...current,
3646
- status: "Canceling..."
3647
- }));
3648
- abortControllerRef.current?.abort();
3649
- }
3650
- function clear() {
3651
- setState({
3652
- imageUri: null,
3653
- result: null,
3654
- progress: 0,
3655
- status: "",
3656
- isGenerating: false,
3657
- error: null
3658
- });
3659
- }
3660
- return {
3661
- ...state,
3662
- run,
3663
- cancel,
3664
- clear
3665
- };
3666
- }
3667
- function useImageModel() {
3668
- const [state, setState] = useAtom(useComposerAtom({
3669
- checking: false,
3670
- state: null,
3671
- error: null
3672
- }));
3673
- async function check() {
3674
- setState((current) => ({
3675
- ...current,
3676
- checking: true,
3677
- error: null
3678
- }));
3679
- try {
3680
- const [modelState] = await listImageModels();
3681
- setState({
3682
- checking: false,
3683
- state: modelState ?? null,
3684
- error: null
3685
- });
3686
- return modelState ?? null;
3687
- } catch (error) {
3688
- setState((current) => ({
3689
- ...current,
3690
- checking: false,
3691
- error: errorFromUnknown(error)
3692
- }));
3693
- return null;
3694
- }
3695
- }
3696
- return {
3697
- ...state,
3698
- installed: state.state?.installed ?? false,
3699
- verified: state.state?.verified ?? false,
3700
- missingFiles: state.state?.missingFiles ?? [],
3701
- bytesPresent: state.state?.bytesPresent ?? 0,
3702
- totalBytes: state.state?.totalBytes ?? 0,
3703
- check
3704
- };
3705
- }
3706
- function useComposerAtom(initialValue) {
3707
- const ref = useStableRef(null);
3708
- ref.current ??= atom(initialValue);
3709
- return ref.current;
3710
- }
3711
- function useStableRef(initialValue) {
3712
- return useRef(initialValue);
3713
- }
3714
- function removeRefFromState(state, index) {
3715
- return {
3716
- ...state,
3717
- refs: state.refs.filter((_, i) => i !== index)
3718
- };
3719
- }
3720
- function inputDestination(inputRoot) {
3721
- return files.directory(inputRoot ?? files.paths.shared, "ImageGenerationInputs", String(Date.now()));
3722
- }
3723
- async function pickOneImage(inputRoot) {
3724
- const [picked] = await files.pick({
3725
- type: "image",
3726
- destination: inputDestination(inputRoot)
3727
- });
3728
- if (!picked) return null;
3729
- return Object.assign(picked, { size: await getImageSize(picked.uri) });
3730
- }
3731
- async function pickImages(inputRoot) {
3732
- const picked = await files.pick({
3733
- type: "image",
3734
- multiple: true,
3735
- destination: inputDestination(inputRoot)
3736
- });
3737
- return Promise.all(picked.map(async (image) => Object.assign(image, { size: await getImageSize(image.uri) })));
3738
- }
3739
- function getImageSize(uri) {
3740
- return new Promise((resolve, reject) => {
3741
- Image.getSize(uri, (width, height) => resolve({
3742
- width,
3743
- height
3744
- }), reject);
3745
- });
3746
- }
3747
- function errorFromUnknown(error) {
3748
- return error instanceof Error ? error : new Error(String(error));
3749
- }
3750
- function modelProgressStatus(progress) {
3751
- return `Model ${Math.round(progress.modelFraction * 100)}% • ${formatBytes(progress.modelBytesWritten)} / ${formatBytes(progress.totalModelBytes)}`;
3752
- }
3753
- function formatBytes(value) {
3754
- if (value < 1024) return `${value} B`;
3755
- const units = [
3756
- "KB",
3757
- "MB",
3758
- "GB",
3759
- "TB"
3760
- ];
3761
- let size = value / 1024;
3762
- let index = 0;
3763
- while (size >= 1024 && index < units.length - 1) {
3764
- size /= 1024;
3765
- index += 1;
3766
- }
3767
- return `${size.toFixed(size >= 10 ? 1 : 2)} ${units[index]}`;
3768
- }
3769
-
3770
- //#endregion
3771
- //#region src/ai/shape.ts
3772
- var ShapeError = class extends Error {
3773
- constructor(code, message, issues = []) {
3774
- super(message);
3775
- this.code = code;
3776
- this.issues = issues;
3777
- this.name = "ShapeError";
3778
- }
3779
- };
3780
- function issue(message, path = []) {
3781
- return {
3782
- message,
3783
- path
3784
- };
3785
- }
3786
- function validateDefinition(definition, value, path = []) {
3787
- if (definition === "string" || definition === "number" || definition === "boolean") {
3788
- if (typeof value === definition) return { value };
3789
- return { issues: [issue(`Expected ${path.join(".") || "value"} to be ${definition}`, path)] };
3790
- }
3791
- if (Array.isArray(definition)) {
3792
- if (!Array.isArray(value)) return { issues: [issue(`Expected ${path.join(".") || "value"} to be array`, path)] };
3793
- const itemDefinition = definition[0];
3794
- const issues$1 = [];
3795
- const output$1 = [];
3796
- for (const [index, item] of value.entries()) {
3797
- const result = validateDefinition(itemDefinition, item, [...path, index]);
3798
- if (result.issues !== void 0) issues$1.push(...result.issues);
3799
- else output$1.push(result.value);
3800
- }
3801
- if (issues$1.length > 0) return { issues: issues$1 };
3802
- return { value: output$1 };
3803
- }
3804
- if (!isRecord(value)) return { issues: [issue(`Expected ${path.join(".") || "value"} to be object`, path)] };
3805
- const output = {};
3806
- const issues = [];
3807
- for (const [key, field] of Object.entries(definition)) {
3808
- const result = validateDefinition(field, value[key], [...path, key]);
3809
- if (result.issues !== void 0) issues.push(...result.issues);
3810
- else output[key] = result.value;
3811
- }
3812
- if (issues.length > 0) return { issues };
3813
- return { value: output };
3814
- }
3815
- function bridgeSchema(definition) {
3816
- if (definition === "string" || definition === "number" || definition === "boolean") return { type: definition };
3817
- if (Array.isArray(definition)) return {
3818
- type: "array",
3819
- items: bridgeSchema(definition[0])
3820
- };
3821
- const properties = {};
3822
- const required = [];
3823
- for (const [key, value] of Object.entries(definition)) {
3824
- properties[key] = bridgeSchema(value);
3825
- required.push(key);
3826
- }
3827
- return {
3828
- type: "object",
3829
- properties,
3830
- required
3831
- };
3832
- }
3833
- function assertValidShape(definition, path = "shape") {
3834
- if (definition === "string" || definition === "number" || definition === "boolean") return;
3835
- if (Array.isArray(definition)) {
3836
- if (definition.length !== 1) throw new ShapeError("invalid_shape", `${path} arrays must contain exactly one item shape`);
3837
- assertValidShape(definition[0], `${path}[0]`);
3838
- return;
3839
- }
3840
- if (!isRecord(definition)) throw new ShapeError("invalid_shape", `${path} must be a shape object`);
3841
- for (const [key, value] of Object.entries(definition)) assertValidShape(value, `${path}.${key}`);
3842
- }
3843
- function createShape(definition) {
3844
- assertValidShape(definition);
3845
- const api = {
3846
- "~standard": {
3847
- version: 1,
3848
- vendor: "bleam",
3849
- validate(value) {
3850
- return api.safeParse(value);
3851
- }
3852
- },
3853
- parse(value) {
3854
- const result = api.safeParse(value);
3855
- if (result.issues !== void 0) throw new ShapeError("validation_failed", result.issues.map((nextIssue) => nextIssue.message).join("\n"), result.issues);
3856
- return result.value;
3857
- },
3858
- safeParse(value) {
3859
- return validateDefinition(definition, value);
3860
- },
3861
- toJSONSchema() {
3862
- return bridgeSchema(definition);
3863
- }
3864
- };
3865
- return api;
3866
- }
3867
- function shape(definition) {
3868
- return createShape(definition);
3869
- }
3870
- function schemaToBridgeSchema(schema) {
3871
- const schemaWithBridge = schema;
3872
- if (typeof schemaWithBridge.toJSONSchema === "function") return schemaWithBridge.toJSONSchema();
3873
- throw new AIError("invalid_schema", "Structured output schemas must implement toJSONSchema()");
3874
- }
3875
- function validateStructuredInput(input) {
3876
- const result = input.shape["~standard"].validate(input.value);
3877
- if (result.issues !== void 0) throw new AIError("validation_failed", result.issues.map((nextIssue) => nextIssue.message).join("\n"), result.issues);
3878
- return result.value;
3879
- }
3880
- function stableStringify(value) {
3881
- if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
3882
- if (isRecord(value)) return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(",")}}`;
3883
- return JSON.stringify(value) ?? "undefined";
3884
- }
3885
-
3886
- //#endregion
3887
- //#region src/ai/text.ts
3888
- function promptFromInput(options) {
3889
- const parts = [];
3890
- if (options.prompt) parts.push(options.prompt);
3891
- if (typeof options.input === "string") parts.push(options.input);
3892
- else if (options.input) parts.push(stableStringify(validateStructuredInput(options.input)));
3893
- if (parts.length === 0) throw new AIError("invalid_message", "Expected prompt or input");
3894
- return parts.join("\n\n");
3895
- }
3896
- async function isAvailable() {
3897
- try {
3898
- return await bridge().isAvailable?.() ?? false;
3899
- } catch {
3900
- return false;
3901
- }
3902
- }
3903
- async function availability() {
3904
- try {
3905
- return await bridge().availability?.() ?? {
3906
- available: false,
3907
- reason: "unknown"
3908
- };
3909
- } catch {
3910
- return {
3911
- available: false,
3912
- reason: "unknown"
3913
- };
3914
- }
3915
- }
3916
- function generationId() {
3917
- return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
3918
- }
3919
- function nativeRequest(options, id) {
3920
- if (options.temperature !== void 0 && (!Number.isFinite(options.temperature) || options.temperature < 0)) throw new AIError("invalid_message", "temperature must be a non-negative number");
3921
- if (options.maxTokens !== void 0 && (!Number.isInteger(options.maxTokens) || options.maxTokens <= 0)) throw new AIError("invalid_message", "maxTokens must be a positive integer");
3922
- const names = /* @__PURE__ */ new Set();
3923
- for (const tool of options.tools ?? []) {
3924
- if (!tool.name.trim()) throw new AIError("invalid_schema", "Tool names must not be empty");
3925
- if (names.has(tool.name)) throw new AIError("invalid_schema", `Duplicate tool name: ${tool.name}`);
3926
- names.add(tool.name);
3927
- }
3928
- return {
3929
- id,
3930
- system: options.system,
3931
- prompt: promptFromInput(options),
3932
- temperature: options.temperature,
3933
- maxTokens: options.maxTokens,
3934
- outputSchema: options.output ? schemaToBridgeSchema(options.output) : void 0,
3935
- tools: options.tools?.map((tool) => ({
3936
- name: tool.name,
3937
- description: tool.description,
3938
- inputSchema: schemaToBridgeSchema(tool.input)
3939
- }))
3940
- };
3941
- }
3942
- function listenForTools(id, tools) {
3943
- if (!tools?.length) return void 0;
3944
- const native = bridge();
3945
- return native.addListener?.("toolCall", (event) => {
3946
- if (event.generationId !== id) return;
3947
- const tool = tools.find((candidate) => candidate.name === event.name);
3948
- (async () => {
3949
- try {
3950
- if (!tool) throw new Error(`Unknown tool: ${event.name}`);
3951
- const validation = tool.input["~standard"].validate(event.arguments);
3952
- if (validation.issues !== void 0) throw new Error(validation.issues.map((issue$1) => issue$1.message).join("\n"));
3953
- const output = await tool.execute(validation.value);
3954
- await native.resolveToolCall?.({
3955
- generationId: id,
3956
- callId: event.callId,
3957
- output: typeof output === "string" ? output : stableStringify(output)
3958
- });
3959
- } catch (error) {
3960
- await native.resolveToolCall?.({
3961
- generationId: id,
3962
- callId: event.callId,
3963
- error: error instanceof Error ? error.message : String(error)
3964
- });
3965
- }
3966
- })();
3967
- });
3968
- }
3969
- async function generate(options) {
3970
- const output = options.output;
3971
- const id = generationId();
3972
- const native = bridge();
3973
- const toolSubscription = listenForTools(id, options.tools);
3974
- const abort = () => void native.cancelTextGeneration?.(id);
3975
- options.signal?.addEventListener("abort", abort, { once: true });
3976
- try {
3977
- if (options.signal?.aborted) throw new AIError("generation_canceled", "Generation canceled");
3978
- const value = await native.generate?.(nativeRequest(options, id));
3979
- if (output) {
3980
- const result = output["~standard"].validate(value);
3981
- if (result.issues !== void 0) throw new AIError("validation_failed", result.issues.map((nextIssue) => nextIssue.message).join("\n"), result.issues);
3982
- return result.value;
3983
- }
3984
- if (typeof value !== "string") throw new AIError("generation_failed", "Expected generated text");
3985
- return value;
3986
- } catch (error) {
3987
- if (error instanceof AIError) throw error;
3988
- throw mapNativeError(error);
3989
- } finally {
3990
- options.signal?.removeEventListener("abort", abort);
3991
- toolSubscription?.remove();
3992
- }
3993
- }
3994
- async function* stream(options) {
3995
- const id = generationId();
3996
- const native = bridge();
3997
- const updates = [];
3998
- let wake;
3999
- let settled = false;
4000
- let failure;
4001
- const updateSubscription = native.addListener?.("textGenerationUpdate", (event) => {
4002
- if (event.id !== id) return;
4003
- updates.push(event.content);
4004
- wake?.();
4005
- wake = void 0;
4006
- });
4007
- const toolSubscription = listenForTools(id, options.tools);
4008
- const abort = () => void native.cancelTextGeneration?.(id);
4009
- options.signal?.addEventListener("abort", abort, { once: true });
4010
- const completion = native.streamGenerate?.(nativeRequest(options, id)).catch((error) => {
4011
- failure = error;
4012
- }).finally(() => {
4013
- settled = true;
4014
- wake?.();
4015
- wake = void 0;
4016
- });
4017
- try {
4018
- if (!completion) throw new AIError("unsupported_runtime", "Streaming is unavailable in this runtime");
4019
- while (!settled || updates.length > 0) if (updates.length > 0) yield updates.shift();
4020
- else await new Promise((resolve) => {
4021
- wake = resolve;
4022
- });
4023
- await completion;
4024
- if (failure) throw mapNativeError(failure);
4025
- } finally {
4026
- if (!settled) await native.cancelTextGeneration?.(id);
4027
- options.signal?.removeEventListener("abort", abort);
4028
- updateSubscription?.remove();
4029
- toolSubscription?.remove();
4030
- }
4031
- }
4032
-
4033
- //#endregion
4034
- //#region src/ai/text-hook.ts
4035
- const initialState = {
4036
- text: "",
4037
- isGenerating: false,
4038
- error: null
4039
- };
4040
- function useTextGeneration() {
4041
- const [state, setState] = useState(initialState);
4042
- const abortControllerRef = useRef(null);
4043
- async function run(options) {
4044
- if (abortControllerRef.current) return null;
4045
- const abortController = new AbortController();
4046
- const abort = () => abortController.abort();
4047
- abortControllerRef.current = abortController;
4048
- options.signal?.addEventListener("abort", abort, { once: true });
4049
- if (options.signal?.aborted) abortController.abort();
4050
- setState({
4051
- text: "",
4052
- isGenerating: true,
4053
- error: null
4054
- });
4055
- let text = "";
4056
- try {
4057
- for await (const snapshot of stream({
4058
- ...options,
4059
- signal: abortController.signal
4060
- })) {
4061
- text = snapshot;
4062
- setState({
4063
- text,
4064
- isGenerating: true,
4065
- error: null
4066
- });
4067
- }
4068
- setState({
4069
- text,
4070
- isGenerating: false,
4071
- error: null
4072
- });
4073
- return text;
4074
- } catch (error) {
4075
- setState({
4076
- text,
4077
- isGenerating: false,
4078
- error: error instanceof Error ? error : new Error(String(error))
4079
- });
4080
- return null;
4081
- } finally {
4082
- options.signal?.removeEventListener("abort", abort);
4083
- if (abortControllerRef.current === abortController) abortControllerRef.current = null;
4084
- }
4085
- }
4086
- function cancel() {
4087
- abortControllerRef.current?.abort();
4088
- }
4089
- function clear() {
4090
- if (abortControllerRef.current) return;
4091
- setState(initialState);
4092
- }
4093
- return {
4094
- ...state,
4095
- run,
4096
- cancel,
4097
- clear
4098
- };
4099
- }
4100
4792
 
4101
4793
  //#endregion
4102
- export { AIError, ImageGenerationError, availability, buildImageGenerationRequest, clampOutpaintRect, defaultImageLongSide, deleteImageLoRA, deleteImageModel, flux2Klein4B8BitAbliterated, fluxImageModelPaths, generate, generateImage, generateImages, getImageGenerationJob, imageMemoryTelemetry, imageModelManifest, imageModelManifests, imageSizeForAspectRatio, imageSizeForPreset, initialOutpaintRect, isAvailable, listImageGenerationJobs, listImageLoRAs, listImageModels, normalizeImageSize, prepareImageLoRA, prepareImageModel, rectForSourceScale, roundImageDimension, roundedImageSize, scheduleImageGeneration, shape, stream, unloadImageModel, upscaleImage, useImageComposer, useImageGeneration, useImageModel, useTextGeneration, verifyImageLoRA, verifyImageModel };
4794
+ export { AIError, ImageGenerationError, bonsai, bonsaiTextModelCatalog, buildImageGenerationRequest, chat, clampOutpaintRect, defaultImageLongSide, deleteImageLoRA, deleteImageModel, deleteBonsaiTextModel as deleteTextModel, flux2Klein4B8BitAbliterated, fluxImageModelPaths, image, imageMemoryTelemetry, imageModelManifest, imageModelManifests, imageSizeForAspectRatio, imageSizeForPreset, initialOutpaintRect, jobs, listImageLoRAs, listImageModels, listBonsaiTextModels as listTextModels, normalizeImageSize, prepareImageLoRA, prepareImageModel, prepareBonsaiTextModel as prepareTextModel, rectForSourceScale, roundImageDimension, roundedImageSize, ternaryBonsaiImage4B, unloadImageModel, useChat, useChatList, useJob, useJobList, verifyImageLoRA, verifyImageModel, verifyBonsaiTextModel as verifyTextModel };