bleam 0.0.10 → 0.0.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/dist/ai.cjs +608 -59
  2. package/dist/ai.d.cts +217 -89
  3. package/dist/ai.d.ts +217 -89
  4. package/dist/ai.js +601 -59
  5. package/dist/cli.cjs +3 -183
  6. package/dist/cli.d.cts +2 -46
  7. package/dist/cli.d.ts +2 -46
  8. package/dist/cli.js +3 -183
  9. package/dist/files.d.cts +1 -1
  10. package/dist/files.d.ts +1 -1
  11. package/dist/platform.cjs +1 -13
  12. package/dist/platform.d.cts +1 -28
  13. package/dist/platform.d.ts +1 -28
  14. package/dist/platform.js +1 -10
  15. package/dist/schema.d.cts +1 -1
  16. package/dist/schema.d.ts +1 -1
  17. package/dist/state.d.cts +1 -1
  18. package/dist/state.d.ts +1 -1
  19. package/dist/{ui-Bg11tvlc.d.ts → ui-1WepaMS4.d.cts} +1 -1
  20. package/dist/{ui-Dd7SXdbg.d.cts → ui-CHc4xEs_.d.ts} +7 -7
  21. package/dist/ui.d.cts +1 -1
  22. package/dist/ui.d.ts +1 -1
  23. package/dist/window.d.cts +1 -1
  24. package/dist/window.d.ts +1 -1
  25. package/package.json +1 -1
  26. package/templates/image-generation/app/index.tsx +3 -1
  27. package/templates/native/ios/Bleam/AI/Flux2/Configuration/ModelRegistry.swift +1 -1
  28. package/templates/native/ios/Bleam/AI/Flux2/Pipeline/FluxImageRunner.swift +7 -2
  29. package/templates/native/ios/Bleam/AI/TextGeneration/Bonsai/BonsaiTextGenerator.swift +46 -0
  30. package/templates/native/ios/Bleam/AI/TextGeneration/Bonsai/BonsaiTextModel.swift +107 -0
  31. package/templates/native/ios/Bleam/AI/TextGeneration/Bonsai/BonsaiTextSnapshotValidator.swift +201 -0
  32. package/templates/native/ios/Bleam/AI/TextGeneration/Core/GenerationTypes.swift +85 -0
  33. package/templates/native/ios/Bleam/AI/TextGeneration/Core/ModelCapabilities.swift +21 -0
  34. package/templates/native/ios/Bleam/AI/TextGeneration/Core/TextKVCache.swift +23 -0
  35. package/templates/native/ios/Bleam/AI/TextGeneration/Core/TokenSampler.swift +79 -0
  36. package/templates/native/ios/Bleam/AI/TextGeneration/Qwen3/Qwen3CheckpointLoader.swift +161 -0
  37. package/templates/native/ios/Bleam/AI/TextGeneration/Qwen3/Qwen3Configuration.swift +119 -0
  38. package/templates/native/ios/Bleam/AI/TextGeneration/Qwen3/Qwen3InferenceSession.swift +84 -0
  39. package/templates/native/ios/Bleam/AI/TextGeneration/Qwen3/Qwen3Layers.swift +197 -0
  40. package/templates/native/ios/Bleam/AI/TextGeneration/Qwen3/Qwen3Model.swift +130 -0
  41. package/templates/native/ios/Bleam/AI/TextGeneration/Qwen35/Qwen35Checkpoint.swift +102 -0
  42. package/templates/native/ios/Bleam/AI/TextGeneration/Qwen35/Qwen35Configuration.swift +140 -0
  43. package/templates/native/ios/Bleam/AI/TextGeneration/Qwen35/Qwen35Model.swift +112 -0
  44. package/templates/native/ios/Bleam/AI/TextGeneration/Qwen35/Qwen35Session.swift +121 -0
  45. package/templates/native/ios/Bleam/AI/TextGeneration/QwenHybrid/QwenHybridCache.swift +42 -0
  46. package/templates/native/ios/Bleam/AI/TextGeneration/QwenHybrid/QwenHybridFullAttention.swift +57 -0
  47. package/templates/native/ios/Bleam/AI/TextGeneration/QwenHybrid/QwenHybridLayers.swift +58 -0
  48. package/templates/native/ios/Bleam/AI/TextGeneration/QwenHybrid/QwenHybridLinearAttention.swift +111 -0
  49. package/templates/native/ios/Bleam/AppDelegate.swift +0 -206
  50. package/templates/native/ios/Bleam.xcodeproj/project.pbxproj +128 -122
  51. package/templates/native/ios/GenerationService/BonsaiTextGenerationRunner.swift +103 -0
  52. package/templates/native/ios/GenerationService/ImageGenerationRunner.swift +19 -7
  53. package/templates/native/ios/GenerationService/main.swift +174 -48
  54. package/templates/native/ios/Shared/Generation/GenerationServiceProtocol.swift +3 -0
  55. package/templates/native/ios/Shared/Generation/GenerationWorkerProtocol.swift +44 -0
  56. package/templates/native/modules/bleam-runtime/ios/AIModule.swift +225 -5
  57. package/templates/native/modules/bleam-runtime/ios/GenerationContracts.swift +47 -0
  58. package/templates/native/modules/bleam-runtime/ios/PlatformModule.swift +0 -191
  59. package/templates/text-generation/app/index.tsx +93 -0
  60. package/templates/text-generation/app.config.ts +5 -0
  61. package/templates/updates/README.md +43 -199
  62. package/templates/updates/src/index.ts +5 -98
  63. package/templates/updates/src/schema.ts +18 -173
  64. package/templates/native/ios/PlatformHelper/main.swift +0 -726
  65. /package/dist/{files-DwA7pzr3.d.cts → files-4ZEoAWiv.d.ts} +0 -0
  66. /package/dist/{files-VrkQlKIT.d.ts → files-Dt5mbzLq.d.cts} +0 -0
  67. /package/dist/{schema-DOOjfXvs.d.ts → schema-CYh6n8GS.d.ts} +0 -0
  68. /package/dist/{schema-ENSMX_1t.d.cts → schema-oeOrd3i1.d.cts} +0 -0
@@ -0,0 +1,103 @@
1
+ import Foundation
2
+ import MLX
3
+
4
+ final class BonsaiTextGenerationRunner: @unchecked Sendable {
5
+ private let request: BonsaiTextGenerationRequest
6
+ private let emit: @Sendable (BonsaiTextGenerationEvent) -> Void
7
+ private let stateLock = NSLock()
8
+ private var cancellationRequested = false
9
+ private var generatedTokenIDs: [Int] = []
10
+
11
+ init(
12
+ request: BonsaiTextGenerationRequest,
13
+ emit: @escaping @Sendable (BonsaiTextGenerationEvent) -> Void
14
+ ) {
15
+ self.request = request
16
+ self.emit = emit
17
+ }
18
+
19
+ func cancel() {
20
+ stateLock.withLock { cancellationRequested = true }
21
+ }
22
+
23
+ func run(isCancelled externalCancellation: @escaping @Sendable () -> Bool) async {
24
+ defer { MLX.Memory.clearCache() }
25
+ do {
26
+ let profile = try BonsaiTextModelProfile.resolve(request.model)
27
+ let parameters = try generationParameters(defaults: profile.defaultGenerationParameters)
28
+ let endTokenIDs = request.endTokenIDs.isEmpty ? profile.defaultEndTokenIDs : Set(request.endTokenIDs)
29
+ let result = try await BonsaiTextGenerator.generate(
30
+ profile: profile,
31
+ modelURL: URL(fileURLWithPath: request.modelPath, isDirectory: true),
32
+ inputIDs: request.inputIDs,
33
+ endTokenIDs: endTokenIDs,
34
+ parameters: parameters,
35
+ isCancelled: { [weak self] in
36
+ externalCancellation() || (self?.isCancellationRequested ?? true)
37
+ },
38
+ onToken: { [weak self, emit, id = request.id] tokenID in
39
+ guard let tokenIDs = self?.append(tokenID) else { return false }
40
+ emit(.init(type: .update, id: id, tokenIDs: tokenIDs))
41
+ return true
42
+ }
43
+ )
44
+
45
+ if result.finishReason == .cancelled || shouldCancel(externalCancellation) {
46
+ emit(.init(type: .canceled, id: request.id, tokenIDs: result.tokenIDs, finishReason: GenerationFinishReason.cancelled.rawValue))
47
+ } else {
48
+ emit(.init(type: .completed, id: request.id, tokenIDs: result.tokenIDs, finishReason: result.finishReason.rawValue))
49
+ }
50
+ } catch is CancellationError {
51
+ emit(.init(type: .canceled, id: request.id, tokenIDs: generatedTokens, finishReason: GenerationFinishReason.cancelled.rawValue))
52
+ } catch {
53
+ emit(.init(type: .failed, id: request.id, tokenIDs: generatedTokens, message: error.localizedDescription))
54
+ }
55
+ }
56
+
57
+ private var isCancellationRequested: Bool {
58
+ stateLock.withLock { cancellationRequested }
59
+ }
60
+
61
+ private var generatedTokens: [Int] {
62
+ stateLock.withLock { generatedTokenIDs }
63
+ }
64
+
65
+ private func append(_ tokenID: Int) -> [Int] {
66
+ stateLock.withLock {
67
+ generatedTokenIDs.append(tokenID)
68
+ return generatedTokenIDs
69
+ }
70
+ }
71
+
72
+ private func shouldCancel(_ externalCancellation: @escaping @Sendable () -> Bool) -> Bool {
73
+ Task.isCancelled || externalCancellation() || isCancellationRequested
74
+ }
75
+
76
+ private func generationParameters(defaults: GenerationParameters) throws -> GenerationParameters {
77
+ let seed: UInt64?
78
+ if let requestedSeed = request.seed {
79
+ guard let converted = UInt64(exactly: requestedSeed) else {
80
+ throw BonsaiTextError.invalidSeed(requestedSeed)
81
+ }
82
+ seed = converted
83
+ } else {
84
+ seed = defaults.seed
85
+ }
86
+
87
+ let parameters = GenerationParameters(
88
+ maximumNewTokens: request.maximumNewTokens ?? defaults.maximumNewTokens,
89
+ temperature: request.temperature.map(Float.init) ?? defaults.temperature,
90
+ topP: request.topP.map(Float.init) ?? defaults.topP,
91
+ topK: request.topK ?? defaults.topK,
92
+ repetitionPenalty: request.repetitionPenalty.map(Float.init) ?? defaults.repetitionPenalty,
93
+ repetitionContextSize: request.repetitionContextSize ?? defaults.repetitionContextSize,
94
+ seed: seed
95
+ )
96
+ try TokenSampler.validate(parameters)
97
+ guard parameters.temperature.isFinite, parameters.topP.isFinite,
98
+ parameters.repetitionPenalty.isFinite else {
99
+ throw BonsaiTextError.invalidGenerationParameters("floating-point values must be finite")
100
+ }
101
+ return parameters
102
+ }
103
+ }
@@ -7,6 +7,7 @@ import UniformTypeIdentifiers
7
7
  final class ImageGenerationRunner: @unchecked Sendable {
8
8
  private let request: GenerationWorkerRequest
9
9
  private let emit: @Sendable (GenerationWorkerEvent) -> Void
10
+ private let stateLock = NSLock()
10
11
  private var isCancelled = false
11
12
 
12
13
  init(
@@ -18,7 +19,7 @@ final class ImageGenerationRunner: @unchecked Sendable {
18
19
  }
19
20
 
20
21
  func cancel() {
21
- isCancelled = true
22
+ stateLock.withLock { isCancelled = true }
22
23
  }
23
24
 
24
25
  func run(isCancelled externalCancellation: @escaping @Sendable () -> Bool) async throws {
@@ -88,10 +89,13 @@ final class ImageGenerationRunner: @unchecked Sendable {
88
89
  // First service version is deliberately memory-first: the runner releases
89
90
  // text encoder, transformer, and VAE stages instead of keeping a warm
90
91
  // pipeline. Add warm caching later only behind explicit idle trimming.
91
- let runner = FluxImageRunner(paths: FluxImageRunnerPaths(
92
- transformer: URL(fileURLWithPath: request.transformerPath, isDirectory: true),
93
- vae: URL(fileURLWithPath: request.vaePath, isDirectory: true)
94
- ))
92
+ let runner = FluxImageRunner(
93
+ paths: FluxImageRunnerPaths(
94
+ transformer: URL(fileURLWithPath: request.transformerPath, isDirectory: true),
95
+ vae: URL(fileURLWithPath: request.vaePath, isDirectory: true)
96
+ ),
97
+ transformerProfile: transformerProfile()
98
+ )
95
99
  var fluxStage = "denoise"
96
100
  do {
97
101
  return try runner.generate(
@@ -147,11 +151,13 @@ final class ImageGenerationRunner: @unchecked Sendable {
147
151
  }
148
152
 
149
153
  private func shouldCancel(_ externalCancellation: @escaping @Sendable () -> Bool) -> Bool {
150
- Task.isCancelled || isCancelled || externalCancellation()
154
+ Task.isCancelled || stateLock.withLock { isCancelled } || externalCancellation()
151
155
  }
152
156
 
153
157
  private func validateRequest() throws {
154
- guard request.model == "flux2-klein-4b-8bit-abliterated" else {
158
+ guard request.model == "flux2-klein-4b-8bit-abliterated"
159
+ || request.model == "ternary-bonsai-image-4b"
160
+ else {
155
161
  throw GenerationWorkerFailure(message: "Unsupported image model: \(request.model)")
156
162
  }
157
163
  guard !request.prompt.isEmpty else {
@@ -264,6 +270,12 @@ final class ImageGenerationRunner: @unchecked Sendable {
264
270
  }
265
271
  }
266
272
 
273
+ private func transformerProfile() -> Flux2TransformerRepoProfile {
274
+ request.model == "ternary-bonsai-image-4b"
275
+ ? ModelRegistry.TransformerRepo.prismMLBonsaiKlein4B2bit.profile
276
+ : ModelRegistry.TransformerRepo.moxinOrgKlein4B8bit.profile
277
+ }
278
+
267
279
  private static func validateSafetensors(atPath path: String) throws {
268
280
  let url = URL(fileURLWithPath: path)
269
281
  let fileSize: UInt64
@@ -2,68 +2,130 @@ import Foundation
2
2
  import MLX
3
3
 
4
4
  private final class GenerationRuntime: @unchecked Sendable {
5
+ private enum OperationKind {
6
+ case image
7
+ case text
8
+ }
9
+
5
10
  private let modelWarmTimeout: Duration = .seconds(60)
11
+ private let stateLock = NSLock()
12
+ private var activeID: String?
13
+ private var activeKind: OperationKind?
6
14
  private var activeTask: Task<Void, Never>?
7
- private var activeRunner: ImageGenerationRunner?
15
+ private var activeImageRunner: ImageGenerationRunner?
16
+ private var activeTextRunner: BonsaiTextGenerationRunner?
8
17
  private var trimTask: Task<Void, Never>?
9
18
  private var isCancelled = false
19
+ private var operationEpoch: UInt64 = 0
20
+ private var isShuttingDown = false
10
21
 
11
22
  var isRunning: Bool {
12
- activeTask != nil
23
+ stateLock.withLock { activeTask != nil }
13
24
  }
14
25
 
15
- func start(
26
+ func startImage(
16
27
  request: GenerationWorkerRequest,
17
28
  emit: @escaping @Sendable (GenerationWorkerEvent) -> Void
18
29
  ) -> Bool {
19
- guard activeTask == nil else { return false }
30
+ stateLock.withLock {
31
+ guard activeTask == nil, !isShuttingDown else { return false }
32
+ trimTask?.cancel()
33
+ trimTask = nil
34
+ isCancelled = false
35
+ operationEpoch &+= 1
36
+ activeID = request.jobID.uuidString
37
+ activeKind = .image
38
+ let runner = ImageGenerationRunner(request: request, emit: emit)
39
+ activeImageRunner = runner
40
+ activeTask = Task { [weak self] in
41
+ await self?.runImage(request, runner: runner, emit: emit)
42
+ }
43
+ return true
44
+ }
45
+ }
46
+
47
+ func startText(
48
+ request: BonsaiTextGenerationRequest,
49
+ emit: @escaping @Sendable (BonsaiTextGenerationEvent) -> Void
50
+ ) -> Bool {
51
+ stateLock.withLock {
52
+ guard activeTask == nil, !isShuttingDown else { return false }
53
+ trimTask?.cancel()
54
+ trimTask = nil
55
+ isCancelled = false
56
+ operationEpoch &+= 1
57
+ activeID = request.id
58
+ activeKind = .text
59
+ let runner = BonsaiTextGenerationRunner(request: request, emit: emit)
60
+ activeTextRunner = runner
61
+ activeTask = Task { [weak self] in
62
+ await self?.runText(request, runner: runner)
63
+ }
64
+ return true
65
+ }
66
+ }
67
+
68
+ func cancelImage() -> Bool {
69
+ let operation = stateLock.withLock { () -> (Task<Void, Never>?, ImageGenerationRunner?)? in
70
+ guard activeKind == .image else { return nil }
71
+ isCancelled = true
72
+ trimTask?.cancel()
73
+ trimTask = nil
74
+ return (activeTask, activeImageRunner)
75
+ }
76
+ guard let operation else { return false }
77
+ operation.0?.cancel()
78
+ operation.1?.cancel()
79
+ return true
80
+ }
20
81
 
21
- trimTask?.cancel()
22
- trimTask = nil
23
- isCancelled = false
24
- activeTask = Task { [weak self] in
25
- await self?.run(request, emit: emit)
82
+ func cancelText(id: String) -> Bool {
83
+ let operation = stateLock.withLock { () -> (Task<Void, Never>?, BonsaiTextGenerationRunner?)? in
84
+ guard activeKind == .text, activeID == id else { return nil }
85
+ isCancelled = true
86
+ trimTask?.cancel()
87
+ trimTask = nil
88
+ return (activeTask, activeTextRunner)
26
89
  }
90
+ guard let operation else { return false }
91
+ operation.0?.cancel()
92
+ operation.1?.cancel()
27
93
  return true
28
94
  }
29
95
 
30
- func cancel() {
31
- isCancelled = true
32
- trimTask?.cancel()
33
- trimTask = nil
34
- activeTask?.cancel()
35
- activeRunner?.cancel()
96
+ func beginShutdown() -> Bool {
97
+ stateLock.withLock {
98
+ guard activeTask == nil, !isShuttingDown else { return false }
99
+ isShuttingDown = true
100
+ operationEpoch &+= 1
101
+ trimTask?.cancel()
102
+ trimTask = nil
103
+ return true
104
+ }
36
105
  }
37
106
 
38
107
  func trimModelsNow() async {
39
- trimTask?.cancel()
40
- trimTask = nil
41
- // We currently reload every stage per generation, but keep the explicit
42
- // trim hook from ImageStudio's service architecture. Warm model caching
43
- // should be added here later, never back in the app process.
44
- MLX.Memory.clearCache()
45
- await Task.yield()
46
- MLX.Memory.clearCache()
108
+ stateLock.withLock {
109
+ trimTask?.cancel()
110
+ trimTask = nil
111
+ MLX.Memory.clearCache()
112
+ MLX.Memory.clearCache()
113
+ }
47
114
  print("[GenerationService] trim active=\(MLX.Memory.activeMemory / 1_048_576)MB cache=\(MLX.Memory.cacheMemory / 1_048_576)MB")
48
115
  }
49
116
 
50
- private func run(
117
+ private func runImage(
51
118
  _ request: GenerationWorkerRequest,
119
+ runner: ImageGenerationRunner,
52
120
  emit: @escaping @Sendable (GenerationWorkerEvent) -> Void
53
121
  ) async {
54
- let runner = ImageGenerationRunner(request: request, emit: emit)
55
- activeRunner = runner
56
122
  defer {
57
- if activeRunner === runner {
58
- activeRunner = nil
59
- }
60
- activeTask = nil
61
- scheduleTrimAfterIdle()
123
+ finish(id: request.jobID.uuidString, imageRunner: runner)
62
124
  }
63
125
 
64
126
  do {
65
127
  try await runner.run(isCancelled: { [weak self] in
66
- Task.isCancelled || (self?.isCancelled ?? false)
128
+ Task.isCancelled || (self?.cancellationRequested ?? false)
67
129
  })
68
130
  } catch is CancellationError {
69
131
  emit(.init(type: .canceled))
@@ -76,17 +138,59 @@ private final class GenerationRuntime: @unchecked Sendable {
76
138
  }
77
139
  }
78
140
 
141
+ private func runText(
142
+ _ request: BonsaiTextGenerationRequest,
143
+ runner: BonsaiTextGenerationRunner
144
+ ) async {
145
+ defer { finish(id: request.id, textRunner: runner) }
146
+ await runner.run(isCancelled: { [weak self] in
147
+ Task.isCancelled || (self?.cancellationRequested ?? true)
148
+ })
149
+ }
150
+
151
+ private var cancellationRequested: Bool {
152
+ stateLock.withLock { isCancelled }
153
+ }
154
+
155
+ private func finish(
156
+ id: String,
157
+ imageRunner: ImageGenerationRunner? = nil,
158
+ textRunner: BonsaiTextGenerationRunner? = nil
159
+ ) {
160
+ let finished = stateLock.withLock {
161
+ guard activeID == id else { return false }
162
+ if let imageRunner, activeImageRunner !== imageRunner { return false }
163
+ if let textRunner, activeTextRunner !== textRunner { return false }
164
+ activeID = nil
165
+ activeKind = nil
166
+ activeImageRunner = nil
167
+ activeTextRunner = nil
168
+ activeTask = nil
169
+ isCancelled = false
170
+ return true
171
+ }
172
+ if finished { scheduleTrimAfterIdle() }
173
+ }
174
+
79
175
  private func scheduleTrimAfterIdle() {
80
- trimTask?.cancel()
81
- trimTask = Task { [weak self] in
82
- guard let self else { return }
83
- do {
84
- try await Task.sleep(for: modelWarmTimeout)
85
- } catch {
86
- return
176
+ stateLock.withLock {
177
+ trimTask?.cancel()
178
+ let scheduledEpoch = operationEpoch
179
+ trimTask = Task { [weak self] in
180
+ guard let self else { return }
181
+ do {
182
+ try await Task.sleep(for: modelWarmTimeout)
183
+ } catch {
184
+ return
185
+ }
186
+ guard !Task.isCancelled else { return }
187
+ self.stateLock.withLock {
188
+ guard self.activeTask == nil, self.operationEpoch == scheduledEpoch else { return }
189
+ self.trimTask = nil
190
+ MLX.Memory.clearCache()
191
+ MLX.Memory.clearCache()
192
+ }
87
193
  }
88
- guard !Task.isCancelled else { return }
89
- await self.trimModelsNow()
90
194
  }
91
195
  print("[GenerationService] model trim scheduled 60s")
92
196
  }
@@ -94,10 +198,11 @@ private final class GenerationRuntime: @unchecked Sendable {
94
198
 
95
199
  private final class GenerationService: NSObject, GenerationServiceProtocol {
96
200
  private weak var connection: NSXPCConnection?
97
- private let runtime = GenerationRuntime()
201
+ private let runtime: GenerationRuntime
98
202
 
99
- init(connection: NSXPCConnection) {
203
+ init(connection: NSXPCConnection, runtime: GenerationRuntime) {
100
204
  self.connection = connection
205
+ self.runtime = runtime
101
206
  }
102
207
 
103
208
  func startGeneration(requestData: Data, reply: @escaping (Bool) -> Void) {
@@ -107,14 +212,27 @@ private final class GenerationService: NSObject, GenerationServiceProtocol {
107
212
  return
108
213
  }
109
214
 
110
- reply(runtime.start(request: request) { [weak self] event in
215
+ reply(runtime.startImage(request: request) { [weak self] event in
111
216
  self?.emit(event)
112
217
  })
113
218
  }
114
219
 
115
220
  func cancelGeneration(reply: @escaping (Bool) -> Void) {
116
- runtime.cancel()
117
- reply(true)
221
+ reply(runtime.cancelImage())
222
+ }
223
+
224
+ func startTextGeneration(requestData: Data, reply: @escaping (Bool) -> Void) {
225
+ guard let request = try? JSONDecoder().decode(BonsaiTextGenerationRequest.self, from: requestData) else {
226
+ reply(false)
227
+ return
228
+ }
229
+ reply(runtime.startText(request: request) { [weak self] event in
230
+ self?.emitText(event)
231
+ })
232
+ }
233
+
234
+ func cancelTextGeneration(id: String, reply: @escaping (Bool) -> Void) {
235
+ reply(runtime.cancelText(id: id))
118
236
  }
119
237
 
120
238
  func memoryTelemetry(reply: @escaping (Data?) -> Void) {
@@ -126,7 +244,7 @@ private final class GenerationService: NSObject, GenerationServiceProtocol {
126
244
  }
127
245
 
128
246
  func shutdown(reply: @escaping (Bool) -> Void) {
129
- guard !runtime.isRunning else {
247
+ guard runtime.beginShutdown() else {
130
248
  reply(false)
131
249
  return
132
250
  }
@@ -142,13 +260,21 @@ private final class GenerationService: NSObject, GenerationServiceProtocol {
142
260
  let client = connection?.remoteObjectProxy as? GenerationServiceClientProtocol
143
261
  client?.generationWorkerDidEmit(eventData: data)
144
262
  }
263
+
264
+ private func emitText(_ event: BonsaiTextGenerationEvent) {
265
+ guard let data = try? JSONEncoder().encode(event) else { return }
266
+ let client = connection?.remoteObjectProxy as? GenerationServiceClientProtocol
267
+ client?.textGenerationWorkerDidEmit(eventData: data)
268
+ }
145
269
  }
146
270
 
147
271
  private final class GenerationServiceDelegate: NSObject, NSXPCListenerDelegate {
272
+ private let runtime = GenerationRuntime()
273
+
148
274
  func listener(_ listener: NSXPCListener, shouldAcceptNewConnection connection: NSXPCConnection) -> Bool {
149
275
  connection.exportedInterface = NSXPCInterface(with: GenerationServiceProtocol.self)
150
276
  connection.remoteObjectInterface = NSXPCInterface(with: GenerationServiceClientProtocol.self)
151
- connection.exportedObject = GenerationService(connection: connection)
277
+ connection.exportedObject = GenerationService(connection: connection, runtime: runtime)
152
278
  connection.resume()
153
279
  return true
154
280
  }
@@ -3,10 +3,13 @@ import Foundation
3
3
  @objc protocol GenerationServiceProtocol {
4
4
  func startGeneration(requestData: Data, reply: @escaping (Bool) -> Void)
5
5
  func cancelGeneration(reply: @escaping (Bool) -> Void)
6
+ func startTextGeneration(requestData: Data, reply: @escaping (Bool) -> Void)
7
+ func cancelTextGeneration(id: String, reply: @escaping (Bool) -> Void)
6
8
  func memoryTelemetry(reply: @escaping (Data?) -> Void)
7
9
  func shutdown(reply: @escaping (Bool) -> Void)
8
10
  }
9
11
 
10
12
  @objc protocol GenerationServiceClientProtocol {
11
13
  func generationWorkerDidEmit(eventData: Data)
14
+ func textGenerationWorkerDidEmit(eventData: Data)
12
15
  }
@@ -122,3 +122,47 @@ struct GenerationWorkerLoRA: Codable, Sendable {
122
122
  let path: String
123
123
  let scale: Double
124
124
  }
125
+
126
+ struct BonsaiTextGenerationRequest: Codable, Sendable {
127
+ let id: String
128
+ let model: String
129
+ let modelPath: String
130
+ let inputIDs: [Int]
131
+ let endTokenIDs: [Int]
132
+ let maximumNewTokens: Int?
133
+ let temperature: Double?
134
+ let topP: Double?
135
+ let topK: Int?
136
+ let repetitionPenalty: Double?
137
+ let repetitionContextSize: Int?
138
+ let seed: Int?
139
+ }
140
+
141
+ struct BonsaiTextGenerationEvent: Codable, Sendable {
142
+ enum EventType: String, Codable, Sendable {
143
+ case update
144
+ case completed
145
+ case canceled
146
+ case failed
147
+ }
148
+
149
+ let type: EventType
150
+ let id: String
151
+ let tokenIDs: [Int]
152
+ let finishReason: String?
153
+ let message: String?
154
+
155
+ init(
156
+ type: EventType,
157
+ id: String,
158
+ tokenIDs: [Int] = [],
159
+ finishReason: String? = nil,
160
+ message: String? = nil
161
+ ) {
162
+ self.type = type
163
+ self.id = id
164
+ self.tokenIDs = tokenIDs
165
+ self.finishReason = finishReason
166
+ self.message = message
167
+ }
168
+ }