bleam 0.0.8 → 0.0.9

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 (87) hide show
  1. package/dist/cli.cjs +23 -4
  2. package/dist/cli.d.cts +2 -0
  3. package/dist/cli.d.ts +2 -0
  4. package/dist/cli.js +23 -4
  5. package/dist/{ui-Ce-pnUUA.d.ts → ui-Bg11tvlc.d.ts} +7 -7
  6. package/dist/ui.d.ts +1 -1
  7. package/dist/window.d.ts +1 -1
  8. package/package.json +4 -1
  9. package/templates/image-generation/app/index.tsx +1 -1
  10. package/templates/native/.gitattributes +1 -0
  11. package/templates/native/App.tsx +26 -0
  12. package/templates/native/app.json +12 -0
  13. package/templates/native/index.ts +8 -0
  14. package/templates/native/ios/.xcode.env +11 -0
  15. package/templates/native/ios/Bleam/AI/Flux2/Configuration/Flux2Config.swift +288 -0
  16. package/templates/native/ios/Bleam/AI/Flux2/Configuration/MemoryConfig.swift +233 -0
  17. package/templates/native/ios/Bleam/AI/Flux2/Configuration/MemoryOptimizationConfig.swift +157 -0
  18. package/templates/native/ios/Bleam/AI/Flux2/Configuration/ModelRegistry.swift +425 -0
  19. package/templates/native/ios/Bleam/AI/Flux2/Configuration/QuantizationConfig.swift +138 -0
  20. package/templates/native/ios/Bleam/AI/Flux2/Configuration/TransformerRepoProfile.swift +59 -0
  21. package/templates/native/ios/Bleam/AI/Flux2/Configuration/VAEConfig.swift +134 -0
  22. package/templates/native/ios/Bleam/AI/Flux2/Flux2Core.swift +55 -0
  23. package/templates/native/ios/Bleam/AI/Flux2/Loading/WeightLoader.swift +1192 -0
  24. package/templates/native/ios/Bleam/AI/Flux2/Pipeline/FluxImageRunner.swift +1282 -0
  25. package/templates/native/ios/Bleam/AI/Flux2/Pipeline/LatentUtils.swift +191 -0
  26. package/templates/native/ios/Bleam/AI/Flux2/Scheduler/FlowMatchEulerScheduler.swift +279 -0
  27. package/templates/native/ios/Bleam/AI/Flux2/Text/KleinTextEncoder.swift +380 -0
  28. package/templates/native/ios/Bleam/AI/Flux2/Transformer/Flux2Attention.swift +379 -0
  29. package/templates/native/ios/Bleam/AI/Flux2/Transformer/Flux2Embeddings.swift +159 -0
  30. package/templates/native/ios/Bleam/AI/Flux2/Transformer/Flux2FeedForward.swift +154 -0
  31. package/templates/native/ios/Bleam/AI/Flux2/Transformer/Flux2FusedKernels.swift +198 -0
  32. package/templates/native/ios/Bleam/AI/Flux2/Transformer/Flux2KVCache.swift +36 -0
  33. package/templates/native/ios/Bleam/AI/Flux2/Transformer/Flux2Modulation.swift +193 -0
  34. package/templates/native/ios/Bleam/AI/Flux2/Transformer/Flux2ParallelAttention.swift +389 -0
  35. package/templates/native/ios/Bleam/AI/Flux2/Transformer/Flux2RoPE.swift +304 -0
  36. package/templates/native/ios/Bleam/AI/Flux2/Transformer/Flux2SingleBlock.swift +290 -0
  37. package/templates/native/ios/Bleam/AI/Flux2/Transformer/Flux2Transformer.swift +530 -0
  38. package/templates/native/ios/Bleam/AI/Flux2/Transformer/Flux2TransformerBlock.swift +362 -0
  39. package/templates/native/ios/Bleam/AI/Flux2/Utils/Flux2Debug.swift +101 -0
  40. package/templates/native/ios/Bleam/AI/Flux2/Utils/MLXCheckpoint.swift +118 -0
  41. package/templates/native/ios/Bleam/AI/Flux2/Utils/MemoryManager.swift +201 -0
  42. package/templates/native/ios/Bleam/AI/Flux2/VAE/AutoencoderKL.swift +476 -0
  43. package/templates/native/ios/Bleam/AI/Flux2/VAE/ResnetBlock.swift +316 -0
  44. package/templates/native/ios/Bleam/AI/Flux2/VAE/VAEDecoder.swift +120 -0
  45. package/templates/native/ios/Bleam/AI/Flux2/VAE/VAEEncoder.swift +136 -0
  46. package/templates/native/ios/Bleam/AppDelegate.swift +373 -0
  47. package/templates/native/ios/Bleam/Appearance.swift +62 -0
  48. package/templates/native/ios/Bleam/Bridging-Header.h +3 -0
  49. package/templates/native/ios/Bleam/Images.xcassets/AppIcon.appiconset/Contents.json +13 -0
  50. package/templates/native/ios/Bleam/Images.xcassets/Contents.json +6 -0
  51. package/templates/native/ios/Bleam/Info.plist +76 -0
  52. package/templates/native/ios/Bleam/PrivacyInfo.xcprivacy +48 -0
  53. package/templates/native/ios/Bleam/SceneDelegate.swift +58 -0
  54. package/templates/native/ios/Bleam/SplashScreen.storyboard +47 -0
  55. package/templates/native/ios/Bleam/Supporting/Expo.plist +6 -0
  56. package/templates/native/ios/Bleam/bleam.entitlements +10 -0
  57. package/templates/native/ios/Bleam.xcodeproj/project.pbxproj +1146 -0
  58. package/templates/native/ios/Bleam.xcodeproj/xcshareddata/xcschemes/Bleam.xcscheme +88 -0
  59. package/templates/native/ios/GenerationService/GenerationService.entitlements +10 -0
  60. package/templates/native/ios/GenerationService/ImageGenerationRunner.swift +591 -0
  61. package/templates/native/ios/GenerationService/Info.plist +31 -0
  62. package/templates/native/ios/GenerationService/main.swift +165 -0
  63. package/templates/native/ios/PlatformHelper/Info.plist +29 -0
  64. package/templates/native/ios/PlatformHelper/main.swift +335 -0
  65. package/templates/native/ios/Podfile +198 -0
  66. package/templates/native/ios/Podfile.lock +2284 -0
  67. package/templates/native/ios/Podfile.properties.json +5 -0
  68. package/templates/native/ios/Shared/Generation/GenerationServiceProtocol.swift +12 -0
  69. package/templates/native/ios/Shared/Generation/GenerationWorkerProtocol.swift +124 -0
  70. package/templates/native/metro.config.js +6 -0
  71. package/templates/native/modules/bleam-runtime/BleamRuntime.podspec +14 -0
  72. package/templates/native/modules/bleam-runtime/expo-module.config.json +8 -0
  73. package/templates/native/modules/bleam-runtime/ios/AIModule.swift +1258 -0
  74. package/templates/native/modules/bleam-runtime/ios/GenerationContracts.swift +135 -0
  75. package/templates/native/modules/bleam-runtime/ios/PlatformModule.swift +313 -0
  76. package/templates/native/modules/bleam-runtime/package.json +5 -0
  77. package/templates/native/package.json +39 -0
  78. package/templates/native/scripts/build/mlx-frameworks.ts +421 -0
  79. package/templates/native/scripts/start/index.ts +474 -0
  80. package/templates/native/scripts/start/known-failures.ts +20 -0
  81. package/templates/native/scripts/start/loader.ts +76 -0
  82. package/templates/native/scripts/start/native-state.ts +242 -0
  83. package/templates/native/scripts/start/package.json +4 -0
  84. package/templates/native/scripts/start/swift-packages.ts +414 -0
  85. package/templates/native/scripts/start/xcode-formatter.ts +106 -0
  86. package/templates/native/tsconfig.json +14 -0
  87. package/templates/native/yarn.lock +3749 -0
@@ -0,0 +1,1258 @@
1
+ import CoreImage
2
+ import CoreImage.CIFilterBuiltins
3
+ import ExpoModulesCore
4
+ import Foundation
5
+
6
+ #if canImport(FoundationModels)
7
+ import FoundationModels
8
+ #endif
9
+
10
+ public final class AIModule: Module {
11
+ @MainActor private let textGeneration = TextGenerationCoordinator()
12
+ private static var appGroupIdentifier: String {
13
+ Bundle.main.object(forInfoDictionaryKey: "AppGroupIdentifier") as? String ?? "group.dev.bleam.app"
14
+ }
15
+
16
+ public func definition() -> ModuleDefinition {
17
+ Name("AI")
18
+ Events("imagegenProgress", "textGenerationUpdate", "toolCall")
19
+
20
+ Function("isAvailable") { () -> Bool in
21
+ #if canImport(FoundationModels)
22
+ if #available(iOS 26, *) {
23
+ return SystemLanguageModel.default.availability == .available
24
+ }
25
+ #endif
26
+ return false
27
+ }
28
+
29
+ Function("availability") { () -> [String: Any] in
30
+ #if canImport(FoundationModels)
31
+ if #available(iOS 26, *) {
32
+ switch SystemLanguageModel.default.availability {
33
+ case .available:
34
+ return ["available": true]
35
+ case .unavailable(.deviceNotEligible):
36
+ return ["available": false, "reason": "device_not_eligible"]
37
+ case .unavailable(.appleIntelligenceNotEnabled):
38
+ return ["available": false, "reason": "apple_intelligence_not_enabled"]
39
+ case .unavailable(.modelNotReady):
40
+ return ["available": false, "reason": "model_not_ready"]
41
+ case .unavailable:
42
+ return ["available": false, "reason": "unknown"]
43
+ }
44
+ }
45
+ #endif
46
+ return ["available": false, "reason": "unknown"]
47
+ }
48
+
49
+ AsyncFunction("generate") { [weak self] (request: TextGenerationRequest, promise: Promise) in
50
+ #if canImport(FoundationModels)
51
+ if #available(iOS 26, *) {
52
+ guard SystemLanguageModel.default.availability == .available else {
53
+ promise.reject("MODEL_UNAVAILABLE", "Apple Intelligence model is not available")
54
+ return
55
+ }
56
+
57
+ guard !request.prompt.isEmpty else {
58
+ promise.reject("INVALID_MESSAGE", "Expected prompt")
59
+ return
60
+ }
61
+
62
+ let generationID = request.id ?? UUID().uuidString
63
+ let task = Task { @MainActor [weak self] in
64
+ guard let self else { return }
65
+ do {
66
+ let tools = try Self.tools(
67
+ from: request.tools,
68
+ generationID: generationID,
69
+ coordinator: self.textGeneration,
70
+ emit: { [weak self] event in self?.sendEvent("toolCall", event) }
71
+ )
72
+ let session = Self.session(system: request.system, tools: tools)
73
+ let options = try Self.generationOptions(from: request)
74
+
75
+ if let schema = request.outputSchema {
76
+ let generationSchema = try Self.generationSchema(from: schema)
77
+ let response = try await session.respond(
78
+ to: request.prompt,
79
+ schema: generationSchema,
80
+ includeSchemaInPrompt: true,
81
+ options: options
82
+ )
83
+ promise.resolve(try Self.value(from: response.content, schema: schema))
84
+ return
85
+ }
86
+
87
+ let response = try await session.respond(to: request.prompt, options: options)
88
+ promise.resolve(response.content)
89
+ } catch {
90
+ Self.reject(error, promise: promise)
91
+ }
92
+ self.textGeneration.finish(generationID)
93
+ }
94
+ Task { @MainActor [weak self] in self?.textGeneration.start(generationID, task: task) }
95
+ } else {
96
+ promise.reject("UNSUPPORTED_OS", "Apple Foundation Models require iOS 26 or newer")
97
+ }
98
+ #else
99
+ promise.reject("UNSUPPORTED_OS", "Apple Foundation Models are unavailable in this build")
100
+ #endif
101
+ }
102
+
103
+ AsyncFunction("streamGenerate") { [weak self] (request: TextGenerationRequest, promise: Promise) in
104
+ #if canImport(FoundationModels)
105
+ if #available(iOS 26, *) {
106
+ guard SystemLanguageModel.default.availability == .available else {
107
+ promise.reject("MODEL_UNAVAILABLE", "Apple Intelligence model is not available")
108
+ return
109
+ }
110
+ guard !request.prompt.isEmpty else {
111
+ promise.reject("INVALID_MESSAGE", "Expected prompt")
112
+ return
113
+ }
114
+
115
+ let generationID = request.id ?? UUID().uuidString
116
+ let task = Task { @MainActor [weak self] in
117
+ guard let self else { return }
118
+ do {
119
+ let tools = try Self.tools(
120
+ from: request.tools,
121
+ generationID: generationID,
122
+ coordinator: self.textGeneration,
123
+ emit: { [weak self] event in self?.sendEvent("toolCall", event) }
124
+ )
125
+ let session = Self.session(system: request.system, tools: tools)
126
+ let options = try Self.generationOptions(from: request)
127
+ let stream = session.streamResponse(to: request.prompt, options: options)
128
+ var content = ""
129
+ for try await snapshot in stream {
130
+ try Task.checkCancellation()
131
+ content = snapshot.content
132
+ self.sendEvent("textGenerationUpdate", ["id": generationID, "content": content])
133
+ }
134
+ promise.resolve(content)
135
+ } catch {
136
+ Self.reject(error, promise: promise)
137
+ }
138
+ self.textGeneration.finish(generationID)
139
+ }
140
+ Task { @MainActor [weak self] in self?.textGeneration.start(generationID, task: task) }
141
+ } else {
142
+ promise.reject("UNSUPPORTED_OS", "Apple Foundation Models require iOS 26 or newer")
143
+ }
144
+ #else
145
+ promise.reject("UNSUPPORTED_OS", "Apple Foundation Models are unavailable in this build")
146
+ #endif
147
+ }
148
+
149
+ AsyncFunction("cancelTextGeneration") { [weak self] (id: String) -> Bool in
150
+ await self?.textGeneration.cancel(id) ?? false
151
+ }
152
+
153
+ AsyncFunction("resolveToolCall") { [weak self] (request: ToolResultRequest) -> Bool in
154
+ await self?.textGeneration.resolve(request) ?? false
155
+ }
156
+
157
+ AsyncFunction("generateImage") { (request: ImageGenerationRequest, promise: Promise) in
158
+ do {
159
+ let imageRequest = try Self.imageRequest(from: request)
160
+ try Self.validateImageRequest(imageRequest)
161
+ let workerRequest = try Self.workerRequest(from: imageRequest)
162
+ Task { @MainActor in
163
+ do {
164
+ let event = try await ImageGenerationServiceClient.shared.run(
165
+ request: workerRequest,
166
+ onProgress: { [weak self] event in
167
+ guard let currentStep = event.currentStep, let totalSteps = event.totalSteps else { return }
168
+ self?.sendEvent("imagegenProgress", [
169
+ "currentStep": currentStep,
170
+ "totalSteps": totalSteps,
171
+ ])
172
+ }
173
+ )
174
+ guard let path = event.path, let width = event.width, let height = event.height, let model = event.model else {
175
+ throw ModuleError.imageGeneration("GenerationService returned an incomplete result")
176
+ }
177
+ var response: [String: Any] = [
178
+ "uri": URL(fileURLWithPath: path).absoluteString,
179
+ "width": width,
180
+ "height": height,
181
+ "model": model,
182
+ ]
183
+ if let metadata = Self.metadataDictionary(event.metadata) {
184
+ response["metadata"] = metadata
185
+ }
186
+ promise.resolve(response)
187
+ } catch {
188
+ Self.rejectImageGeneration(error, promise: promise)
189
+ }
190
+ }
191
+ } catch {
192
+ promise.reject(error)
193
+ }
194
+ }
195
+
196
+ AsyncFunction("cancelImagegen") { (promise: Promise) in
197
+ Task { @MainActor in
198
+ do {
199
+ promise.resolve(try await ImageGenerationServiceClient.shared.cancel())
200
+ } catch {
201
+ promise.reject(error)
202
+ }
203
+ }
204
+ }
205
+
206
+ AsyncFunction("imageMemoryTelemetry") { (promise: Promise) in
207
+ Task { @MainActor in
208
+ do {
209
+ promise.resolve(try await ImageGenerationServiceClient.shared.memoryTelemetry())
210
+ } catch {
211
+ promise.reject(error)
212
+ }
213
+ }
214
+ }
215
+
216
+ AsyncFunction("unloadImageModel") { (promise: Promise) in
217
+ Task { @MainActor in
218
+ do {
219
+ promise.resolve(try await ImageGenerationServiceClient.shared.unload())
220
+ } catch {
221
+ promise.reject(error)
222
+ }
223
+ }
224
+ }
225
+
226
+ AsyncFunction("upscaleImage") { (request: UpscaleImageRequest) throws -> [String: Any] in
227
+ guard request.scale > 1, request.scale <= 4, request.scale.isFinite else {
228
+ throw ModuleError.invalidModel("Upscale scale must be greater than 1 and at most 4")
229
+ }
230
+ let sourceURL = URL(fileURLWithPath: request.imagePath)
231
+ guard sourceURL.isFileURL, FileManager.default.fileExists(atPath: sourceURL.path) else {
232
+ throw ModuleError.invalidMessage("Upscale image does not exist")
233
+ }
234
+ guard let source = CIImage(contentsOf: sourceURL) else {
235
+ throw ModuleError.invalidMessage("Upscale image could not be decoded")
236
+ }
237
+ let width = Int((source.extent.width * request.scale).rounded())
238
+ let height = Int((source.extent.height * request.scale).rounded())
239
+ guard width > 0, height > 0, width <= 16_384, height <= 16_384 else {
240
+ throw ModuleError.invalidModel("Upscaled dimensions must not exceed 16384 pixels")
241
+ }
242
+
243
+ let filter = CIFilter.lanczosScaleTransform()
244
+ filter.inputImage = source
245
+ filter.scale = Float(request.scale)
246
+ filter.aspectRatio = 1
247
+ guard let output = filter.outputImage else {
248
+ throw ModuleError.imageGeneration("Image upscaling failed")
249
+ }
250
+
251
+ let directory = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0]
252
+ .appendingPathComponent("Bleam/Upscaled", isDirectory: true)
253
+ try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
254
+ let outputURL = directory.appendingPathComponent("\(UUID().uuidString.lowercased()).png")
255
+ let colorSpace = source.colorSpace ?? CGColorSpace(name: CGColorSpace.sRGB)!
256
+ try CIContext().writePNGRepresentation(
257
+ of: output,
258
+ to: outputURL,
259
+ format: .RGBA8,
260
+ colorSpace: colorSpace
261
+ )
262
+ return [
263
+ "uri": outputURL.absoluteString,
264
+ "width": width,
265
+ "height": height,
266
+ "scale": request.scale,
267
+ ]
268
+ }
269
+ }
270
+
271
+ private static func imageRequest(from request: ImageGenerationRequest) throws -> FluxImageRequest {
272
+ guard !request.model.isEmpty else {
273
+ throw ModuleError.invalidModel("Missing image model")
274
+ }
275
+ guard request.model == "flux2-klein-4b-8bit-abliterated" else {
276
+ throw ModuleError.invalidModel("Unsupported image model: \(request.model)")
277
+ }
278
+ guard !request.prompt.isEmpty else {
279
+ throw ModuleError.invalidMessage("Expected prompt")
280
+ }
281
+ if let sourceImagePath = request.sourceImagePath, sourceImagePath.isEmpty {
282
+ throw ModuleError.invalidMessage("Source image path must not be empty")
283
+ }
284
+ if let maskImagePath = request.maskImagePath, maskImagePath.isEmpty {
285
+ throw ModuleError.invalidMessage("Mask image path must not be empty")
286
+ }
287
+ if request.maskImagePath != nil, request.sourceImagePath == nil {
288
+ throw ModuleError.invalidMessage("Mask image requires a source image")
289
+ }
290
+ if let maskMode = request.maskMode, maskMode != "paint" && maskMode != "preserve" {
291
+ throw ModuleError.invalidModel("Mask mode must be paint or preserve")
292
+ }
293
+ if request.outpaintWidth != nil || request.outpaintHeight != nil || request.outpaintSourceX != nil || request.outpaintSourceY != nil || request.outpaintSourceWidth != nil || request.outpaintSourceHeight != nil {
294
+ guard request.sourceImagePath != nil else {
295
+ throw ModuleError.invalidMessage("Outpaint requires a source image")
296
+ }
297
+ guard let outpaintWidth = request.outpaintWidth, outpaintWidth > 0 else {
298
+ throw ModuleError.invalidModel("Invalid outpaint width")
299
+ }
300
+ guard let outpaintHeight = request.outpaintHeight, outpaintHeight > 0 else {
301
+ throw ModuleError.invalidModel("Invalid outpaint height")
302
+ }
303
+ guard let sourceX = request.outpaintSourceX,
304
+ let sourceY = request.outpaintSourceY,
305
+ let sourceWidth = request.outpaintSourceWidth, sourceWidth > 0,
306
+ let sourceHeight = request.outpaintSourceHeight, sourceHeight > 0
307
+ else {
308
+ throw ModuleError.invalidModel("Outpaint requires a valid source rect")
309
+ }
310
+ guard sourceX < outpaintWidth, sourceY < outpaintHeight,
311
+ sourceX + sourceWidth > 0, sourceY + sourceHeight > 0
312
+ else {
313
+ throw ModuleError.invalidModel("Outpaint source rect must intersect the canvas")
314
+ }
315
+ if request.outpaintAutoCrop != true,
316
+ (sourceX < 0 || sourceY < 0 || sourceX + sourceWidth > outpaintWidth || sourceY + sourceHeight > outpaintHeight)
317
+ {
318
+ throw ModuleError.invalidModel("Outpaint source rect must fit within the canvas unless autoCrop is enabled")
319
+ }
320
+ }
321
+ if let resizeMode = request.resizeMode, !["stretch", "fit", "crop"].contains(resizeMode) {
322
+ throw ModuleError.invalidModel("Resize mode must be stretch, fit, or crop")
323
+ }
324
+ if let maxInputLongSide = request.maxInputLongSide, maxInputLongSide != 0 && maxInputLongSide < 64 {
325
+ throw ModuleError.invalidModel("maxInputLongSide must be 0 or at least 64")
326
+ }
327
+ guard request.referenceImagePaths.allSatisfy({ !$0.isEmpty }) else {
328
+ throw ModuleError.invalidMessage("Reference image paths must not be empty")
329
+ }
330
+ guard request.referenceImagePaths.count <= 3 else {
331
+ throw ModuleError.invalidModel("Expected at most 3 reference images")
332
+ }
333
+ if let referenceInfluences = request.referenceInfluences {
334
+ guard referenceInfluences.count == request.referenceImagePaths.count else {
335
+ throw ModuleError.invalidModel("Reference influences must match reference image count")
336
+ }
337
+ guard referenceInfluences.allSatisfy({ $0.isFinite && $0 >= 0 }) else {
338
+ throw ModuleError.invalidModel("Reference influences must be non-negative numbers")
339
+ }
340
+ }
341
+ guard request.loraPaths.allSatisfy({ !$0.isEmpty }) else {
342
+ throw ModuleError.invalidMessage("LoRA paths must not be empty")
343
+ }
344
+ if let loraScales = request.loraScales {
345
+ guard loraScales.count == request.loraPaths.count else {
346
+ throw ModuleError.invalidModel("LoRA scales must match LoRA path count")
347
+ }
348
+ guard loraScales.allSatisfy({ $0.isFinite && $0 > 0 }) else {
349
+ throw ModuleError.invalidModel("LoRA scales must be positive numbers")
350
+ }
351
+ }
352
+ if let loraIds = request.loraIds {
353
+ guard loraIds.count == request.loraPaths.count else {
354
+ throw ModuleError.invalidModel("LoRA ids must match LoRA path count")
355
+ }
356
+ }
357
+ guard !request.modelRoot.isEmpty else {
358
+ throw ModuleError.invalidModel("Missing model root")
359
+ }
360
+ guard !request.textEncoderPath.isEmpty else {
361
+ throw ModuleError.invalidModel("Missing text encoder path")
362
+ }
363
+ guard !request.transformerPath.isEmpty else {
364
+ throw ModuleError.invalidModel("Missing transformer path")
365
+ }
366
+ guard !request.vaePath.isEmpty else {
367
+ throw ModuleError.invalidModel("Missing VAE path")
368
+ }
369
+ guard request.width > 0 else {
370
+ throw ModuleError.invalidModel("Invalid image width")
371
+ }
372
+ guard request.height > 0 else {
373
+ throw ModuleError.invalidModel("Invalid image height")
374
+ }
375
+ guard request.steps > 0 else {
376
+ throw ModuleError.invalidModel("Invalid image steps")
377
+ }
378
+ guard request.guidance > 0 else {
379
+ throw ModuleError.invalidModel("Invalid image guidance")
380
+ }
381
+ if let strength = request.strength, !(strength > 0 && strength <= 1) {
382
+ throw ModuleError.invalidModel("Image strength must be greater than 0 and at most 1")
383
+ }
384
+ guard !request.inputIds.isEmpty else {
385
+ throw ModuleError.invalidMessage("Missing tokenized input ids")
386
+ }
387
+ guard request.attentionMask.count == request.inputIds.count else {
388
+ throw ModuleError.invalidMessage("Attention mask must match input ids")
389
+ }
390
+
391
+ return FluxImageRequest(
392
+ model: request.model,
393
+ prompt: request.prompt,
394
+ sourceImagePath: request.sourceImagePath,
395
+ maskImagePath: request.maskImagePath,
396
+ maskMode: request.maskMode,
397
+ outpaintWidth: request.outpaintWidth,
398
+ outpaintHeight: request.outpaintHeight,
399
+ outpaintSourceX: request.outpaintSourceX,
400
+ outpaintSourceY: request.outpaintSourceY,
401
+ outpaintSourceWidth: request.outpaintSourceWidth,
402
+ outpaintSourceHeight: request.outpaintSourceHeight,
403
+ outpaintAutoCrop: request.outpaintAutoCrop,
404
+ resizeMode: request.resizeMode,
405
+ maxInputLongSide: request.maxInputLongSide,
406
+ referenceImagePaths: request.referenceImagePaths,
407
+ referenceInfluences: request.referenceInfluences,
408
+ loraPaths: request.loraPaths,
409
+ loraScales: request.loraScales,
410
+ loraIds: request.loraIds,
411
+ sigmaSchedule: request.sigmaSchedule,
412
+ width: request.width,
413
+ height: request.height,
414
+ steps: request.steps,
415
+ guidance: request.guidance,
416
+ strength: request.strength,
417
+ seed: request.seed,
418
+ inputIds: request.inputIds,
419
+ attentionMask: request.attentionMask,
420
+ modelRoot: request.modelRoot,
421
+ textEncoderPath: request.textEncoderPath,
422
+ transformerPath: request.transformerPath,
423
+ vaePath: request.vaePath
424
+ )
425
+ }
426
+
427
+ private static func validateImageRequest(_ request: FluxImageRequest) throws {
428
+ let fileManager = FileManager.default
429
+ for path in [request.modelRoot, request.textEncoderPath, request.transformerPath, request.vaePath] {
430
+ var isDirectory: ObjCBool = false
431
+ guard fileManager.fileExists(atPath: path, isDirectory: &isDirectory), isDirectory.boolValue else {
432
+ throw ModuleError.invalidModel("Missing model directory: \(path)")
433
+ }
434
+ }
435
+
436
+ for path in [request.sourceImagePath, request.maskImagePath].compactMap({ $0 }) + request.referenceImagePaths + request.loraPaths {
437
+ var isDirectory: ObjCBool = false
438
+ guard fileManager.fileExists(atPath: path, isDirectory: &isDirectory), !isDirectory.boolValue else {
439
+ throw ModuleError.invalidMessage("Missing image file: \(path)")
440
+ }
441
+ }
442
+ }
443
+
444
+ private static func workerRequest(from request: FluxImageRequest) throws -> GenerationWorkerRequest {
445
+ let jobID = UUID()
446
+ guard let appGroupContainer = FileManager.default.containerURL(
447
+ forSecurityApplicationGroupIdentifier: appGroupIdentifier
448
+ ) else {
449
+ throw ModuleError.imageGeneration("Shared generation container is unavailable")
450
+ }
451
+
452
+ let stagingDirectory = appGroupContainer
453
+ .appendingPathComponent("GenerationRuns", isDirectory: true)
454
+ .appendingPathComponent("bleam-generation-jobs", isDirectory: true)
455
+ .appendingPathComponent(jobID.uuidString, isDirectory: true)
456
+ try FileManager.default.createDirectory(at: stagingDirectory, withIntermediateDirectories: true)
457
+
458
+ return GenerationWorkerRequest(
459
+ jobID: jobID,
460
+ model: request.model,
461
+ prompt: request.prompt,
462
+ sourceImagePath: request.sourceImagePath,
463
+ maskImagePath: request.maskImagePath,
464
+ maskMode: request.maskMode,
465
+ outpaintWidth: request.outpaintWidth,
466
+ outpaintHeight: request.outpaintHeight,
467
+ outpaintSourceX: request.outpaintSourceX,
468
+ outpaintSourceY: request.outpaintSourceY,
469
+ outpaintSourceWidth: request.outpaintSourceWidth,
470
+ outpaintSourceHeight: request.outpaintSourceHeight,
471
+ outpaintAutoCrop: request.outpaintAutoCrop,
472
+ resizeMode: request.resizeMode,
473
+ maxInputLongSide: request.maxInputLongSide,
474
+ referenceImagePaths: request.referenceImagePaths,
475
+ referenceInfluences: request.referenceInfluences,
476
+ loraPaths: request.loraPaths,
477
+ loraScales: request.loraScales,
478
+ loraIds: request.loraIds,
479
+ sigmaSchedule: request.sigmaSchedule,
480
+ width: request.width,
481
+ height: request.height,
482
+ steps: request.steps,
483
+ guidance: request.guidance,
484
+ strength: request.strength,
485
+ seed: request.seed,
486
+ inputIds: request.inputIds,
487
+ attentionMask: request.attentionMask,
488
+ modelRoot: request.modelRoot,
489
+ textEncoderPath: request.textEncoderPath,
490
+ transformerPath: request.transformerPath,
491
+ vaePath: request.vaePath,
492
+ stagingDirectoryPath: stagingDirectory.path
493
+ )
494
+ }
495
+
496
+ private static func metadataDictionary(_ metadata: GenerationWorkerMetadata?) -> [String: Any]? {
497
+ guard let metadata else { return nil }
498
+ var value: [String: Any] = [
499
+ "model": metadata.model,
500
+ "width": metadata.width,
501
+ "height": metadata.height,
502
+ "steps": metadata.steps,
503
+ "guidance": metadata.guidance,
504
+ "elapsedMs": metadata.elapsedMs,
505
+ ]
506
+ if let strength = metadata.strength { value["strength"] = strength }
507
+ if let seed = metadata.seed { value["seed"] = seed }
508
+ if let appliedLoRAs = metadata.appliedLoRAs {
509
+ value["appliedLoRAs"] = appliedLoRAs.map { lora in
510
+ var item: [String: Any] = [
511
+ "path": lora.path,
512
+ "scale": lora.scale,
513
+ ]
514
+ if let id = lora.id { item["id"] = id }
515
+ return item
516
+ }
517
+ }
518
+ if let sigmaSchedule = metadata.sigmaSchedule {
519
+ value["sigmaSchedule"] = sigmaSchedule
520
+ }
521
+ return value
522
+ }
523
+
524
+ private static func rejectImageGeneration(_ error: Error, promise: Promise) {
525
+ if let failure = error as? GenerationWorkerFailure {
526
+ let message = failure.stage.map { "[\($0)] \(failure.message)" } ?? failure.message
527
+ promise.reject("IMAGE_GENERATION_ERROR", message)
528
+ return
529
+ }
530
+ promise.reject(error)
531
+ }
532
+
533
+ #if canImport(FoundationModels)
534
+ @available(iOS 26, *)
535
+ private static func session(system: String?, tools: [any Tool]) -> LanguageModelSession {
536
+ if let system, !system.isEmpty {
537
+ return LanguageModelSession(tools: tools, instructions: system)
538
+ }
539
+ return LanguageModelSession(tools: tools)
540
+ }
541
+
542
+ @available(iOS 26, *)
543
+ private static func tools(
544
+ from definitions: [ToolDefinitionRequest],
545
+ generationID: String,
546
+ coordinator: TextGenerationCoordinator,
547
+ emit: @escaping @MainActor @Sendable ([String: Any]) -> Void
548
+ ) throws -> [any Tool] {
549
+ try definitions.map { definition in
550
+ let parameters = try generationSchema(from: definition.inputSchema)
551
+ return DynamicTool(
552
+ name: definition.name,
553
+ description: definition.description,
554
+ parameters: parameters,
555
+ generationID: generationID,
556
+ coordinator: coordinator,
557
+ emit: emit
558
+ )
559
+ }
560
+ }
561
+
562
+ @available(iOS 26, *)
563
+ private static func generationOptions(from request: TextGenerationRequest) throws -> GenerationOptions {
564
+ return GenerationOptions(
565
+ sampling: request.temperature == nil ? .greedy : nil,
566
+ temperature: request.temperature,
567
+ maximumResponseTokens: request.maxTokens
568
+ )
569
+ }
570
+
571
+ @available(iOS 26, *)
572
+ private static func generationSchema(from schema: [String: Any]) throws -> GenerationSchema {
573
+ do {
574
+ return try GenerationSchema(root: dynamicSchema(from: schema), dependencies: [])
575
+ } catch {
576
+ throw ModuleError.invalidSchema(error.localizedDescription)
577
+ }
578
+ }
579
+
580
+ @available(iOS 26, *)
581
+ private static func dynamicSchema(from schema: [String: Any]) throws -> DynamicGenerationSchema {
582
+ guard let type = schema["type"] as? String else {
583
+ throw ModuleError.invalidSchema("Missing schema type")
584
+ }
585
+
586
+ switch type {
587
+ case "object":
588
+ return try objectSchema(from: schema)
589
+ case "array":
590
+ guard let itemSchema = schema["items"] as? [String: Any] else {
591
+ throw ModuleError.invalidSchema("Array schema must include items")
592
+ }
593
+ return try DynamicGenerationSchema(arrayOf: dynamicSchema(from: itemSchema))
594
+ case "string":
595
+ return DynamicGenerationSchema(type: String.self)
596
+ case "number":
597
+ return DynamicGenerationSchema(type: Double.self)
598
+ case "boolean":
599
+ return DynamicGenerationSchema(type: Bool.self)
600
+ default:
601
+ throw ModuleError.invalidSchema("Unsupported schema type: \(type)")
602
+ }
603
+ }
604
+
605
+ @available(iOS 26, *)
606
+ private static func objectSchema(from schema: [String: Any]) throws -> DynamicGenerationSchema {
607
+ let propertiesDictionary = schema["properties"] as? [String: Any] ?? [:]
608
+ let required = Set(schema["required"] as? [String] ?? [])
609
+ var properties: [DynamicGenerationSchema.Property] = []
610
+
611
+ for key in propertiesDictionary.keys.sorted() {
612
+ guard let propertySchema = propertiesDictionary[key] as? [String: Any] else {
613
+ throw ModuleError.invalidSchema("Property \(key) schema must be an object")
614
+ }
615
+
616
+ properties.append(
617
+ DynamicGenerationSchema.Property(
618
+ name: key,
619
+ description: nil,
620
+ schema: try dynamicSchema(from: propertySchema),
621
+ isOptional: !required.contains(key)
622
+ )
623
+ )
624
+ }
625
+
626
+ return DynamicGenerationSchema(name: schema["title"] as? String ?? "Output", properties: properties)
627
+ }
628
+
629
+ @available(iOS 26, *)
630
+ private static func value(from content: GeneratedContent, schema: [String: Any]) throws -> Any {
631
+ guard let type = schema["type"] as? String else {
632
+ throw ModuleError.invalidSchema("Missing schema type")
633
+ }
634
+
635
+ switch type {
636
+ case "object":
637
+ let propertiesDictionary = schema["properties"] as? [String: Any] ?? [:]
638
+ let output = NSMutableDictionary()
639
+
640
+ for key in propertiesDictionary.keys.sorted() {
641
+ guard let propertySchema = propertiesDictionary[key] as? [String: Any] else {
642
+ throw ModuleError.invalidSchema("Property \(key) schema must be an object")
643
+ }
644
+ output[key] = try propertyValue(from: content, property: key, schema: propertySchema)
645
+ }
646
+
647
+ return output
648
+ default:
649
+ throw ModuleError.invalidSchema("Root schema must be an object")
650
+ }
651
+ }
652
+
653
+ @available(iOS 26, *)
654
+ private static func propertyValue(
655
+ from content: GeneratedContent,
656
+ property: String,
657
+ schema: [String: Any]
658
+ ) throws -> Any {
659
+ guard let type = schema["type"] as? String else {
660
+ throw ModuleError.invalidSchema("Missing schema type")
661
+ }
662
+
663
+ switch type {
664
+ case "string":
665
+ return try content.value(String.self, forProperty: property)
666
+ case "number":
667
+ return try content.value(Double.self, forProperty: property)
668
+ case "boolean":
669
+ return try content.value(Bool.self, forProperty: property)
670
+ case "object":
671
+ let nestedContent = try content.value(GeneratedContent.self, forProperty: property)
672
+ return try value(from: nestedContent, schema: schema)
673
+ case "array":
674
+ guard let itemSchema = schema["items"] as? [String: Any], let itemType = itemSchema["type"] as? String else {
675
+ throw ModuleError.invalidSchema("Array schema must include items")
676
+ }
677
+
678
+ switch itemType {
679
+ case "string":
680
+ return try content.value([String].self, forProperty: property)
681
+ case "number":
682
+ return try content.value([Double].self, forProperty: property)
683
+ case "boolean":
684
+ return try content.value([Bool].self, forProperty: property)
685
+ case "object":
686
+ let values = try content.value([GeneratedContent].self, forProperty: property)
687
+ return try values.map { try value(from: $0, schema: itemSchema) }
688
+ default:
689
+ throw ModuleError.invalidSchema("Unsupported array item type: \(itemType)")
690
+ }
691
+ default:
692
+ throw ModuleError.invalidSchema("Unsupported schema type: \(type)")
693
+ }
694
+ }
695
+
696
+ @available(iOS 26, *)
697
+ private static func reject(_ error: Error, promise: Promise) {
698
+ if error is CancellationError {
699
+ promise.reject("GENERATION_CANCELED", "Generation canceled")
700
+ return
701
+ }
702
+ if let generationError = error as? LanguageModelSession.GenerationError {
703
+ switch generationError {
704
+ case .exceededContextWindowSize:
705
+ promise.reject("CONTEXT_WINDOW_EXCEEDED", error.localizedDescription)
706
+ case .concurrentRequests:
707
+ promise.reject("CONCURRENT_REQUEST", error.localizedDescription)
708
+ case .guardrailViolation:
709
+ promise.reject("GUARDRAIL_VIOLATION", error.localizedDescription)
710
+ case .unsupportedLanguageOrLocale:
711
+ promise.reject("UNSUPPORTED_LANGUAGE_OR_LOCALE", error.localizedDescription)
712
+ case .rateLimited:
713
+ promise.reject("RATE_LIMITED", error.localizedDescription)
714
+ default:
715
+ promise.reject("GENERATION_ERROR", error.localizedDescription)
716
+ }
717
+ return
718
+ }
719
+ if let toolError = error as? LanguageModelSession.ToolCallError {
720
+ promise.reject("TOOL_ERROR", toolError.localizedDescription)
721
+ return
722
+ }
723
+
724
+ promise.reject(error)
725
+ }
726
+ #endif
727
+ }
728
+
729
+ #if canImport(FoundationModels)
730
+ @available(iOS 26, *)
731
+ private struct DynamicTool: Tool {
732
+ typealias Arguments = GeneratedContent
733
+
734
+ let name: String
735
+ let description: String
736
+ let parameters: GenerationSchema
737
+ let generationID: String
738
+ let coordinator: TextGenerationCoordinator
739
+ let emit: @MainActor @Sendable ([String: Any]) -> Void
740
+
741
+ func call(arguments: GeneratedContent) async throws -> String {
742
+ let data = Data(arguments.jsonString.utf8)
743
+ let value = try JSONSerialization.jsonObject(with: data)
744
+ return try await coordinator.call(
745
+ generationID: generationID,
746
+ name: name,
747
+ arguments: value,
748
+ emit: emit
749
+ )
750
+ }
751
+ }
752
+ #endif
753
+
754
+ private struct ToolExecutionError: LocalizedError {
755
+ let message: String
756
+ var errorDescription: String? { message }
757
+ }
758
+
759
+ @MainActor
760
+ private final class TextGenerationCoordinator {
761
+ private struct PendingToolCall {
762
+ let generationID: String
763
+ let continuation: CheckedContinuation<String, Error>
764
+ }
765
+
766
+ private var tasks: [String: Task<Void, Never>] = [:]
767
+ private var toolCalls: [String: PendingToolCall] = [:]
768
+
769
+ func start(_ id: String, task: Task<Void, Never>) {
770
+ tasks[id]?.cancel()
771
+ tasks[id] = task
772
+ }
773
+
774
+ func finish(_ id: String) {
775
+ tasks[id] = nil
776
+ failToolCalls(for: id, error: CancellationError())
777
+ }
778
+
779
+ func cancel(_ id: String) -> Bool {
780
+ guard let task = tasks.removeValue(forKey: id) else { return false }
781
+ task.cancel()
782
+ failToolCalls(for: id, error: CancellationError())
783
+ return true
784
+ }
785
+
786
+ func call(
787
+ generationID: String,
788
+ name: String,
789
+ arguments: Any,
790
+ emit: @escaping @MainActor @Sendable ([String: Any]) -> Void
791
+ ) async throws -> String {
792
+ try Task.checkCancellation()
793
+ let callID = UUID().uuidString
794
+ return try await withCheckedThrowingContinuation { continuation in
795
+ toolCalls[callID] = PendingToolCall(generationID: generationID, continuation: continuation)
796
+ emit([
797
+ "generationId": generationID,
798
+ "callId": callID,
799
+ "name": name,
800
+ "arguments": arguments,
801
+ ])
802
+ }
803
+ }
804
+
805
+ func resolve(_ request: ToolResultRequest) -> Bool {
806
+ guard let pending = toolCalls.removeValue(forKey: request.callId),
807
+ pending.generationID == request.generationId
808
+ else { return false }
809
+
810
+ if let error = request.error {
811
+ pending.continuation.resume(throwing: ToolExecutionError(message: error))
812
+ } else {
813
+ pending.continuation.resume(returning: request.output ?? "")
814
+ }
815
+ return true
816
+ }
817
+
818
+ private func failToolCalls(for generationID: String, error: Error) {
819
+ let matches = toolCalls.filter { $0.value.generationID == generationID }
820
+ for (id, pending) in matches {
821
+ toolCalls[id] = nil
822
+ pending.continuation.resume(throwing: error)
823
+ }
824
+ }
825
+ }
826
+
827
+ private enum ModuleError: Error, LocalizedError {
828
+ case invalidSchema(String)
829
+ case invalidMessage(String)
830
+ case invalidModel(String)
831
+ case imageGeneration(String)
832
+
833
+ var errorDescription: String? {
834
+ switch self {
835
+ case .invalidSchema(let message):
836
+ return "Invalid schema: \(message)"
837
+ case .invalidMessage(let message), .invalidModel(let message), .imageGeneration(let message):
838
+ return message
839
+ }
840
+ }
841
+ }
842
+
843
+ @MainActor
844
+ private final class ImageGenerationServiceClient {
845
+ static let shared = ImageGenerationServiceClient()
846
+
847
+ private static var serviceName: String {
848
+ "\(Bundle.main.bundleIdentifier ?? "dev.bleam.app").GenerationService"
849
+ }
850
+ private var connection: NSXPCConnection?
851
+ private var pendingRun: PendingRun?
852
+ private let eventReceiver = GenerationServiceEventReceiver()
853
+
854
+ private final class PendingRun {
855
+ let continuation: CheckedContinuation<GenerationWorkerEvent, Error>
856
+ let onProgress: (GenerationWorkerEvent) -> Void
857
+
858
+ init(
859
+ continuation: CheckedContinuation<GenerationWorkerEvent, Error>,
860
+ onProgress: @escaping (GenerationWorkerEvent) -> Void
861
+ ) {
862
+ self.continuation = continuation
863
+ self.onProgress = onProgress
864
+ }
865
+ }
866
+
867
+ func run(
868
+ request: GenerationWorkerRequest,
869
+ onProgress: @escaping (GenerationWorkerEvent) -> Void = { _ in }
870
+ ) async throws -> GenerationWorkerEvent {
871
+ let terminalEvent: GenerationWorkerEvent = try await withCheckedThrowingContinuation { continuation in
872
+ do {
873
+ guard pendingRun == nil else {
874
+ continuation.resume(throwing: GenerationServiceClientError.concurrentRequest)
875
+ return
876
+ }
877
+
878
+ eventReceiver.onEvent = { [weak self] event in
879
+ self?.deliver(event)
880
+ }
881
+ pendingRun = PendingRun(continuation: continuation, onProgress: onProgress)
882
+
883
+ let requestData = try JSONEncoder().encode(request)
884
+ let service = try remoteProxy(errorHandler: { [weak self] error in
885
+ Task { @MainActor in
886
+ self?.failPendingRun(Self.mapConnectionError(error))
887
+ }
888
+ })
889
+ service.startGeneration(requestData: requestData) { [weak self] accepted in
890
+ Task { @MainActor in
891
+ guard accepted else {
892
+ self?.failPendingRun(GenerationServiceClientError.serviceUnavailable)
893
+ return
894
+ }
895
+ }
896
+ }
897
+ } catch {
898
+ pendingRun = nil
899
+ eventReceiver.onEvent = nil
900
+ continuation.resume(throwing: error)
901
+ }
902
+ }
903
+
904
+ try handleTerminalEvent(terminalEvent)
905
+ return terminalEvent
906
+ }
907
+
908
+ func cancel() async throws -> Bool {
909
+ guard pendingRun != nil else {
910
+ return false
911
+ }
912
+
913
+ let service = try remoteProxy(errorHandler: { [weak self] error in
914
+ Task { @MainActor in
915
+ self?.failPendingRun(Self.mapConnectionError(error))
916
+ }
917
+ })
918
+
919
+ return try await withCheckedThrowingContinuation { continuation in
920
+ service.cancelGeneration { canceled in
921
+ continuation.resume(returning: canceled)
922
+ }
923
+ }
924
+ }
925
+
926
+ func memoryTelemetry() async throws -> [String: Any] {
927
+ let service = try remoteProxy(errorHandler: { _ in })
928
+ return try await withCheckedThrowingContinuation { continuation in
929
+ service.memoryTelemetry { data in
930
+ guard
931
+ let data,
932
+ let value = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
933
+ else {
934
+ continuation.resume(throwing: GenerationServiceClientError.serviceUnavailable)
935
+ return
936
+ }
937
+ continuation.resume(returning: value)
938
+ }
939
+ }
940
+ }
941
+
942
+ func unload() async throws -> Bool {
943
+ guard pendingRun == nil else { return false }
944
+ let service = try remoteProxy(errorHandler: { _ in })
945
+ let unloaded = try await withCheckedThrowingContinuation { continuation in
946
+ service.shutdown { accepted in
947
+ continuation.resume(returning: accepted)
948
+ }
949
+ }
950
+ if unloaded {
951
+ connection?.invalidate()
952
+ connection = nil
953
+ }
954
+ return unloaded
955
+ }
956
+
957
+ private func remoteProxy(errorHandler: @escaping (Error) -> Void) throws -> GenerationServiceProtocol {
958
+ if connection == nil {
959
+ let nextConnection = try Self.makeConnection(serviceName: Self.serviceName)
960
+ nextConnection.remoteObjectInterface = NSXPCInterface(with: GenerationServiceProtocol.self)
961
+ nextConnection.exportedInterface = NSXPCInterface(with: GenerationServiceClientProtocol.self)
962
+ nextConnection.exportedObject = eventReceiver
963
+ nextConnection.invalidationHandler = { [weak self] in
964
+ Task { @MainActor in self?.handleConnectionLost() }
965
+ }
966
+ nextConnection.interruptionHandler = { [weak self] in
967
+ Task { @MainActor in self?.handleConnectionLost() }
968
+ }
969
+ nextConnection.resume()
970
+ connection = nextConnection
971
+ }
972
+
973
+ guard let service = connection?.remoteObjectProxyWithErrorHandler(errorHandler) as? GenerationServiceProtocol else {
974
+ throw GenerationServiceClientError.serviceUnavailable
975
+ }
976
+ return service
977
+ }
978
+
979
+ private static func makeConnection(serviceName: String) throws -> NSXPCConnection {
980
+ #if targetEnvironment(macCatalyst)
981
+ let connectionClass: AnyObject = NSXPCConnection.self as AnyObject
982
+ let allocSelector = NSSelectorFromString("alloc")
983
+ let initSelector = NSSelectorFromString("initWithServiceName:")
984
+
985
+ guard
986
+ let allocated = connectionClass.perform(allocSelector)?.takeUnretainedValue() as AnyObject?,
987
+ allocated.responds(to: initSelector),
988
+ let connection = allocated.perform(initSelector, with: serviceName)?.takeUnretainedValue() as? NSXPCConnection
989
+ else {
990
+ throw GenerationServiceClientError.serviceUnavailable
991
+ }
992
+
993
+ return connection
994
+ #else
995
+ return NSXPCConnection(serviceName: serviceName)
996
+ #endif
997
+ }
998
+
999
+ private func deliver(_ event: GenerationWorkerEvent) {
1000
+ if event.type == .log {
1001
+ DevLogPoster.post(
1002
+ source: "xpc",
1003
+ level: event.level ?? "info",
1004
+ category: event.category,
1005
+ message: event.message ?? "",
1006
+ jobID: event.jobID
1007
+ )
1008
+ return
1009
+ }
1010
+
1011
+ guard let pendingRun else {
1012
+ print("[GenerationServiceClient] event without pending request type=\(event.type.rawValue)")
1013
+ return
1014
+ }
1015
+
1016
+ if event.type == .progress {
1017
+ pendingRun.onProgress(event)
1018
+ return
1019
+ }
1020
+
1021
+ if event.type == .completed || event.type == .canceled || event.type == .failed {
1022
+ self.pendingRun = nil
1023
+ eventReceiver.onEvent = nil
1024
+ pendingRun.continuation.resume(returning: event)
1025
+ }
1026
+ }
1027
+
1028
+ private func failPendingRun(_ error: Error) {
1029
+ guard let pendingRun else { return }
1030
+ self.pendingRun = nil
1031
+ eventReceiver.onEvent = nil
1032
+ if GenerationServiceClientError.isServiceKilledInsufficientMemory(error) {
1033
+ invalidate()
1034
+ }
1035
+ pendingRun.continuation.resume(throwing: error)
1036
+ }
1037
+
1038
+ private func handleConnectionLost() {
1039
+ connection = nil
1040
+ failPendingRun(GenerationServiceClientError.serviceInterrupted)
1041
+ }
1042
+
1043
+ private func invalidate() {
1044
+ connection?.invalidate()
1045
+ connection = nil
1046
+ }
1047
+
1048
+ private func handleTerminalEvent(_ event: GenerationWorkerEvent) throws {
1049
+ switch event.type {
1050
+ case .completed:
1051
+ return
1052
+ case .canceled:
1053
+ throw CancellationError()
1054
+ case .failed:
1055
+ throw GenerationWorkerFailure(message: event.message ?? "Generation failed.", stage: event.stage)
1056
+ default:
1057
+ throw GenerationServiceClientError.invalidResponse
1058
+ }
1059
+ }
1060
+
1061
+ private static func mapConnectionError(_ error: Error) -> Error {
1062
+ let nsError = error as NSError
1063
+ if nsError.domain == NSCocoaErrorDomain || nsError.domain.localizedCaseInsensitiveContains("xpc") {
1064
+ return GenerationServiceClientError.serviceInterrupted
1065
+ }
1066
+ return error
1067
+ }
1068
+ }
1069
+
1070
+ private enum DevLogPoster {
1071
+ static func post(source: String, level: String, category: String?, message: String, jobID: String? = nil) {
1072
+ Task.detached {
1073
+ guard let config = try? loadProjectConfig(), let devServer = config["devServer"] as? String else {
1074
+ return
1075
+ }
1076
+ guard let url = URL(string: "\(devServer)/bleam/logs") else {
1077
+ return
1078
+ }
1079
+
1080
+ var payload: [String: Any] = [
1081
+ "source": source,
1082
+ "level": level,
1083
+ "message": message,
1084
+ ]
1085
+ if let category { payload["category"] = category }
1086
+ if let jobID { payload["jobId"] = jobID }
1087
+
1088
+ guard let body = try? JSONSerialization.data(withJSONObject: payload) else {
1089
+ return
1090
+ }
1091
+ var request = URLRequest(url: url)
1092
+ request.httpMethod = "POST"
1093
+ request.setValue("application/json", forHTTPHeaderField: "Content-Type")
1094
+ request.httpBody = body
1095
+ _ = try? await URLSession.shared.data(for: request)
1096
+ }
1097
+ }
1098
+
1099
+ private static func loadProjectConfig() throws -> [String: Any]? {
1100
+ guard let configPath = projectConfigPath() else {
1101
+ return nil
1102
+ }
1103
+
1104
+ let data = try Data(contentsOf: URL(fileURLWithPath: configPath))
1105
+ return try JSONSerialization.jsonObject(with: data) as? [String: Any]
1106
+ }
1107
+
1108
+ private static func projectConfigPath() -> String? {
1109
+ let arguments = ProcessInfo.processInfo.arguments
1110
+ guard let flagIndex = arguments.firstIndex(of: "--bleam-project-config") else {
1111
+ return nil
1112
+ }
1113
+
1114
+ let valueIndex = arguments.index(after: flagIndex)
1115
+ guard arguments.indices.contains(valueIndex) else {
1116
+ return nil
1117
+ }
1118
+
1119
+ return arguments[valueIndex]
1120
+ }
1121
+ }
1122
+
1123
+ private enum GenerationServiceClientError: LocalizedError, Equatable {
1124
+ case serviceUnavailable
1125
+ case invalidResponse
1126
+ case serviceInterrupted
1127
+ case serviceKilledInsufficientMemory
1128
+ case concurrentRequest
1129
+
1130
+ static func isServiceKilledInsufficientMemory(_ error: Error) -> Bool {
1131
+ guard let clientError = error as? GenerationServiceClientError else { return false }
1132
+ return clientError == .serviceKilledInsufficientMemory
1133
+ }
1134
+
1135
+ var errorDescription: String? {
1136
+ switch self {
1137
+ case .serviceUnavailable:
1138
+ return "GenerationService is unavailable."
1139
+ case .invalidResponse:
1140
+ return "GenerationService returned an invalid response."
1141
+ case .serviceInterrupted:
1142
+ return "GenerationService was interrupted."
1143
+ case .serviceKilledInsufficientMemory:
1144
+ return "Generation failed because there was not enough available memory."
1145
+ case .concurrentRequest:
1146
+ return "GenerationService is already running a request."
1147
+ }
1148
+ }
1149
+ }
1150
+
1151
+ private final class GenerationServiceEventReceiver: NSObject, GenerationServiceClientProtocol {
1152
+ @MainActor var onEvent: ((GenerationWorkerEvent) -> Void)?
1153
+
1154
+ func generationWorkerDidEmit(eventData: Data) {
1155
+ guard let event = try? JSONDecoder().decode(GenerationWorkerEvent.self, from: eventData) else { return }
1156
+ Task { @MainActor in
1157
+ onEvent?(event)
1158
+ }
1159
+ }
1160
+ }
1161
+
1162
+ private struct TextGenerationRequest: Record {
1163
+ @Field var id: String?
1164
+ @Field var system: String?
1165
+ @Field var prompt: String = ""
1166
+ @Field var outputSchema: [String: Any]?
1167
+ @Field var temperature: Double?
1168
+ @Field var maxTokens: Int?
1169
+ @Field var tools: [ToolDefinitionRequest] = []
1170
+ }
1171
+
1172
+ private struct ToolDefinitionRequest: Record {
1173
+ @Field var name: String = ""
1174
+ @Field var description: String = ""
1175
+ @Field var inputSchema: [String: Any] = [:]
1176
+ }
1177
+
1178
+ private struct ToolResultRequest: Record {
1179
+ @Field var generationId: String = ""
1180
+ @Field var callId: String = ""
1181
+ @Field var output: String?
1182
+ @Field var error: String?
1183
+ }
1184
+
1185
+ private struct ImageGenerationRequest: Record {
1186
+ @Field var model: String = ""
1187
+ @Field var prompt: String = ""
1188
+ @Field var sourceImagePath: String?
1189
+ @Field var maskImagePath: String?
1190
+ @Field var maskMode: String?
1191
+ @Field var outpaintWidth: Int?
1192
+ @Field var outpaintHeight: Int?
1193
+ @Field var outpaintSourceX: Int?
1194
+ @Field var outpaintSourceY: Int?
1195
+ @Field var outpaintSourceWidth: Int?
1196
+ @Field var outpaintSourceHeight: Int?
1197
+ @Field var outpaintAutoCrop: Bool?
1198
+ @Field var resizeMode: String?
1199
+ @Field var maxInputLongSide: Int?
1200
+ @Field var referenceImagePaths: [String] = []
1201
+ @Field var referenceInfluences: [Double]?
1202
+ @Field var loraPaths: [String] = []
1203
+ @Field var loraScales: [Double]?
1204
+ @Field var loraIds: [String]?
1205
+ @Field var sigmaSchedule: [Double]?
1206
+ @Field var width: Int = 0
1207
+ @Field var height: Int = 0
1208
+ @Field var steps: Int = 0
1209
+ @Field var guidance: Double = 0
1210
+ @Field var strength: Double?
1211
+ @Field var seed: Int?
1212
+ @Field var inputIds: [Int] = []
1213
+ @Field var attentionMask: [Int] = []
1214
+ @Field var modelRoot: String = ""
1215
+ @Field var textEncoderPath: String = ""
1216
+ @Field var transformerPath: String = ""
1217
+ @Field var vaePath: String = ""
1218
+ }
1219
+
1220
+ private struct UpscaleImageRequest: Record {
1221
+ @Field var imagePath: String = ""
1222
+ @Field var scale: Double = 2
1223
+ }
1224
+
1225
+ private struct FluxImageRequest {
1226
+ let model: String
1227
+ let prompt: String
1228
+ let sourceImagePath: String?
1229
+ let maskImagePath: String?
1230
+ let maskMode: String?
1231
+ let outpaintWidth: Int?
1232
+ let outpaintHeight: Int?
1233
+ let outpaintSourceX: Int?
1234
+ let outpaintSourceY: Int?
1235
+ let outpaintSourceWidth: Int?
1236
+ let outpaintSourceHeight: Int?
1237
+ let outpaintAutoCrop: Bool?
1238
+ let resizeMode: String?
1239
+ let maxInputLongSide: Int?
1240
+ let referenceImagePaths: [String]
1241
+ let referenceInfluences: [Double]?
1242
+ let loraPaths: [String]
1243
+ let loraScales: [Double]?
1244
+ let loraIds: [String]?
1245
+ let sigmaSchedule: [Double]?
1246
+ let width: Int
1247
+ let height: Int
1248
+ let steps: Int
1249
+ let guidance: Double
1250
+ let strength: Double?
1251
+ let seed: Int?
1252
+ let inputIds: [Int]
1253
+ let attentionMask: [Int]
1254
+ let modelRoot: String
1255
+ let textEncoderPath: String
1256
+ let transformerPath: String
1257
+ let vaePath: String
1258
+ }