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.
- package/dist/ai.cjs +1872 -1182
- package/dist/ai.d.cts +267 -234
- package/dist/ai.d.ts +267 -234
- package/dist/ai.js +1859 -1167
- package/dist/cli.cjs +3 -183
- package/dist/cli.d.cts +3 -47
- package/dist/cli.d.ts +3 -47
- package/dist/cli.js +3 -183
- package/dist/config.d.cts +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/{files-Ds1wT8C2.js → files-BXVkPrPN.js} +6 -1
- package/dist/{files-Bo7h9fik.cjs → files-DxaQ-Nv0.cjs} +11 -0
- package/dist/files.cjs +1 -1
- package/dist/files.js +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/platform.cjs +1 -13
- package/dist/platform.d.cts +1 -28
- package/dist/platform.d.ts +1 -28
- package/dist/platform.js +1 -10
- package/dist/schema-B5BfdswF.js +226 -0
- package/dist/schema-BnVZOXfu.cjs +286 -0
- package/dist/schema-D5eImHxu.d.cts +125 -0
- package/dist/schema-SSjokbtw.d.ts +125 -0
- package/dist/schema.cjs +10 -2
- package/dist/schema.d.cts +2 -2
- package/dist/schema.d.ts +2 -2
- package/dist/schema.js +2 -2
- package/dist/state-Dh3HLixb.js +874 -0
- package/dist/state-LssDgpff.cjs +973 -0
- package/dist/state.cjs +16 -12
- package/dist/state.d.cts +144 -140
- package/dist/state.d.ts +145 -140
- package/dist/state.js +3 -3
- package/dist/{ui-Bg11tvlc.d.ts → ui-1WepaMS4.d.cts} +1 -1
- package/dist/{ui-Dd7SXdbg.d.cts → ui-D7bRLYee.d.ts} +7 -7
- package/dist/ui.d.cts +1 -1
- package/dist/ui.d.ts +1 -1
- package/dist/window.d.cts +1 -1
- package/dist/window.d.ts +1 -1
- package/package.json +3 -1
- package/templates/foundation-models/app/index.tsx +77 -15
- package/templates/image-generation/app/index.tsx +4 -2
- package/templates/native/ios/Bleam/AI/Flux2/Configuration/ModelRegistry.swift +1 -1
- package/templates/native/ios/Bleam/AI/Flux2/Pipeline/FluxImageRunner.swift +7 -2
- package/templates/native/ios/Bleam/AI/TextGeneration/Bonsai/BonsaiTextGenerator.swift +46 -0
- package/templates/native/ios/Bleam/AI/TextGeneration/Bonsai/BonsaiTextModel.swift +107 -0
- package/templates/native/ios/Bleam/AI/TextGeneration/Bonsai/BonsaiTextSnapshotValidator.swift +201 -0
- package/templates/native/ios/Bleam/AI/TextGeneration/Core/GenerationTypes.swift +85 -0
- package/templates/native/ios/Bleam/AI/TextGeneration/Core/ModelCapabilities.swift +21 -0
- package/templates/native/ios/Bleam/AI/TextGeneration/Core/TextKVCache.swift +23 -0
- package/templates/native/ios/Bleam/AI/TextGeneration/Core/TokenSampler.swift +79 -0
- package/templates/native/ios/Bleam/AI/TextGeneration/Qwen3/Qwen3CheckpointLoader.swift +161 -0
- package/templates/native/ios/Bleam/AI/TextGeneration/Qwen3/Qwen3Configuration.swift +119 -0
- package/templates/native/ios/Bleam/AI/TextGeneration/Qwen3/Qwen3InferenceSession.swift +84 -0
- package/templates/native/ios/Bleam/AI/TextGeneration/Qwen3/Qwen3Layers.swift +197 -0
- package/templates/native/ios/Bleam/AI/TextGeneration/Qwen3/Qwen3Model.swift +130 -0
- package/templates/native/ios/Bleam/AI/TextGeneration/Qwen35/Qwen35Checkpoint.swift +102 -0
- package/templates/native/ios/Bleam/AI/TextGeneration/Qwen35/Qwen35Configuration.swift +140 -0
- package/templates/native/ios/Bleam/AI/TextGeneration/Qwen35/Qwen35Model.swift +112 -0
- package/templates/native/ios/Bleam/AI/TextGeneration/Qwen35/Qwen35Session.swift +121 -0
- package/templates/native/ios/Bleam/AI/TextGeneration/QwenHybrid/QwenHybridCache.swift +42 -0
- package/templates/native/ios/Bleam/AI/TextGeneration/QwenHybrid/QwenHybridFullAttention.swift +57 -0
- package/templates/native/ios/Bleam/AI/TextGeneration/QwenHybrid/QwenHybridLayers.swift +58 -0
- package/templates/native/ios/Bleam/AI/TextGeneration/QwenHybrid/QwenHybridLinearAttention.swift +111 -0
- package/templates/native/ios/Bleam/AppDelegate.swift +0 -206
- package/templates/native/ios/Bleam.xcodeproj/project.pbxproj +135 -129
- package/templates/native/ios/GenerationService/BonsaiTextGenerationRunner.swift +103 -0
- package/templates/native/ios/GenerationService/ImageGenerationRunner.swift +19 -7
- package/templates/native/ios/GenerationService/main.swift +174 -48
- package/templates/native/ios/Podfile.lock +173 -173
- package/templates/native/ios/Shared/Generation/GenerationServiceProtocol.swift +3 -0
- package/templates/native/ios/Shared/Generation/GenerationWorkerProtocol.swift +44 -0
- package/templates/native/modules/bleam-runtime/ios/AIModule.swift +239 -341
- package/templates/native/modules/bleam-runtime/ios/GenerationContracts.swift +47 -0
- package/templates/native/modules/bleam-runtime/ios/PlatformModule.swift +0 -191
- package/templates/text-generation/app/index.tsx +124 -0
- package/templates/text-generation/app.config.ts +5 -0
- package/templates/updates/README.md +43 -199
- package/templates/updates/src/index.ts +5 -98
- package/templates/updates/src/schema.ts +18 -173
- package/dist/schema-Bo5Jvqus.js +0 -90
- package/dist/schema-DOOjfXvs.d.ts +0 -58
- package/dist/schema-ENSMX_1t.d.cts +0 -58
- package/dist/schema-rQ13mrpD.cjs +0 -102
- package/dist/state-Bx0VlTlO.cjs +0 -852
- package/dist/state-CAwe-Vw1.js +0 -767
- package/templates/native/ios/PlatformHelper/main.swift +0 -726
- /package/dist/{config-Cms0rvqg.d.ts → config-COcRnn5a.d.cts} +0 -0
- /package/dist/{config-CufOVJV3.d.cts → config-Chi-flpJ.d.ts} +0 -0
|
@@ -15,90 +15,7 @@ public final class AIModule: Module {
|
|
|
15
15
|
|
|
16
16
|
public func definition() -> ModuleDefinition {
|
|
17
17
|
Name("AI")
|
|
18
|
-
Events("imagegenProgress", "textGenerationUpdate", "
|
|
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
|
-
}
|
|
18
|
+
Events("imagegenProgress", "textGenerationUpdate", "localTextGenerationUpdate")
|
|
102
19
|
|
|
103
20
|
AsyncFunction("streamGenerate") { [weak self] (request: TextGenerationRequest, promise: Promise) in
|
|
104
21
|
#if canImport(FoundationModels)
|
|
@@ -116,13 +33,7 @@ public final class AIModule: Module {
|
|
|
116
33
|
let task = Task { @MainActor [weak self] in
|
|
117
34
|
guard let self else { return }
|
|
118
35
|
do {
|
|
119
|
-
let
|
|
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)
|
|
36
|
+
let session = try Self.session(system: request.system, messages: request.messages)
|
|
126
37
|
let options = try Self.generationOptions(from: request)
|
|
127
38
|
let stream = session.streamResponse(to: request.prompt, options: options)
|
|
128
39
|
var content = ""
|
|
@@ -147,11 +58,28 @@ public final class AIModule: Module {
|
|
|
147
58
|
}
|
|
148
59
|
|
|
149
60
|
AsyncFunction("cancelTextGeneration") { [weak self] (id: String) -> Bool in
|
|
150
|
-
await self?.textGeneration.cancel(id) ?? false
|
|
61
|
+
let foundationCanceled = await self?.textGeneration.cancel(id) ?? false
|
|
62
|
+
let localCanceled = (try? await ImageGenerationServiceClient.shared.cancelText(id: id)) ?? false
|
|
63
|
+
return foundationCanceled || localCanceled
|
|
151
64
|
}
|
|
152
65
|
|
|
153
|
-
AsyncFunction("
|
|
154
|
-
|
|
66
|
+
AsyncFunction("streamLocalText") { [weak self] (request: LocalTextGenerationRequest, promise: Promise) in
|
|
67
|
+
Task { @MainActor in
|
|
68
|
+
do {
|
|
69
|
+
let event = try await ImageGenerationServiceClient.shared.runText(
|
|
70
|
+
request: Self.bonsaiTextRequest(from: request),
|
|
71
|
+
onUpdate: { [weak self] event in
|
|
72
|
+
self?.sendEvent("localTextGenerationUpdate", [
|
|
73
|
+
"id": event.id,
|
|
74
|
+
"tokenIds": event.tokenIDs,
|
|
75
|
+
])
|
|
76
|
+
}
|
|
77
|
+
)
|
|
78
|
+
promise.resolve(event.tokenIDs)
|
|
79
|
+
} catch {
|
|
80
|
+
Self.rejectLocalTextGeneration(error, promise: promise)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
155
83
|
}
|
|
156
84
|
|
|
157
85
|
AsyncFunction("generateImage") { (request: ImageGenerationRequest, promise: Promise) in
|
|
@@ -272,12 +200,23 @@ public final class AIModule: Module {
|
|
|
272
200
|
guard !request.model.isEmpty else {
|
|
273
201
|
throw ModuleError.invalidModel("Missing image model")
|
|
274
202
|
}
|
|
275
|
-
guard request.model == "flux2-klein-4b-8bit-abliterated"
|
|
203
|
+
guard request.model == "flux2-klein-4b-8bit-abliterated"
|
|
204
|
+
|| request.model == "ternary-bonsai-image-4b"
|
|
205
|
+
else {
|
|
276
206
|
throw ModuleError.invalidModel("Unsupported image model: \(request.model)")
|
|
277
207
|
}
|
|
278
208
|
guard !request.prompt.isEmpty else {
|
|
279
209
|
throw ModuleError.invalidMessage("Expected prompt")
|
|
280
210
|
}
|
|
211
|
+
if request.model == "ternary-bonsai-image-4b",
|
|
212
|
+
request.sourceImagePath != nil
|
|
213
|
+
|| request.maskImagePath != nil
|
|
214
|
+
|| request.outpaintWidth != nil
|
|
215
|
+
|| !request.referenceImagePaths.isEmpty
|
|
216
|
+
|| !request.loraPaths.isEmpty
|
|
217
|
+
{
|
|
218
|
+
throw ModuleError.invalidModel("Ternary Bonsai Image 4B currently supports text-to-image generation")
|
|
219
|
+
}
|
|
281
220
|
if let sourceImagePath = request.sourceImagePath, sourceImagePath.isEmpty {
|
|
282
221
|
throw ModuleError.invalidMessage("Source image path must not be empty")
|
|
283
222
|
}
|
|
@@ -493,6 +432,40 @@ public final class AIModule: Module {
|
|
|
493
432
|
)
|
|
494
433
|
}
|
|
495
434
|
|
|
435
|
+
private static func bonsaiTextRequest(
|
|
436
|
+
from request: LocalTextGenerationRequest
|
|
437
|
+
) throws -> BonsaiTextGenerationRequest {
|
|
438
|
+
guard !request.id.isEmpty else {
|
|
439
|
+
throw ModuleError.invalidMessage("Expected generation id")
|
|
440
|
+
}
|
|
441
|
+
guard !request.model.isEmpty else {
|
|
442
|
+
throw ModuleError.invalidModel("Expected Bonsai model id")
|
|
443
|
+
}
|
|
444
|
+
var isDirectory: ObjCBool = false
|
|
445
|
+
guard FileManager.default.fileExists(atPath: request.modelPath, isDirectory: &isDirectory),
|
|
446
|
+
isDirectory.boolValue
|
|
447
|
+
else {
|
|
448
|
+
throw ModuleError.invalidModel("Missing Bonsai model directory: \(request.modelPath)")
|
|
449
|
+
}
|
|
450
|
+
guard !request.inputIds.isEmpty else {
|
|
451
|
+
throw ModuleError.invalidMessage("Expected tokenized prompt")
|
|
452
|
+
}
|
|
453
|
+
return BonsaiTextGenerationRequest(
|
|
454
|
+
id: request.id,
|
|
455
|
+
model: request.model,
|
|
456
|
+
modelPath: request.modelPath,
|
|
457
|
+
inputIDs: request.inputIds,
|
|
458
|
+
endTokenIDs: request.endTokenIds,
|
|
459
|
+
maximumNewTokens: request.maxTokens,
|
|
460
|
+
temperature: request.temperature,
|
|
461
|
+
topP: request.topP,
|
|
462
|
+
topK: request.topK,
|
|
463
|
+
repetitionPenalty: request.repetitionPenalty,
|
|
464
|
+
repetitionContextSize: request.repetitionContextSize,
|
|
465
|
+
seed: request.seed
|
|
466
|
+
)
|
|
467
|
+
}
|
|
468
|
+
|
|
496
469
|
private static func metadataDictionary(_ metadata: GenerationWorkerMetadata?) -> [String: Any]? {
|
|
497
470
|
guard let metadata else { return nil }
|
|
498
471
|
var value: [String: Any] = [
|
|
@@ -530,33 +503,53 @@ public final class AIModule: Module {
|
|
|
530
503
|
promise.reject(error)
|
|
531
504
|
}
|
|
532
505
|
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
506
|
+
private static func rejectLocalTextGeneration(_ error: Error, promise: Promise) {
|
|
507
|
+
if error is CancellationError {
|
|
508
|
+
promise.reject("GENERATION_CANCELED", "Generation canceled")
|
|
509
|
+
return
|
|
510
|
+
}
|
|
511
|
+
if error as? GenerationServiceClientError == .concurrentRequest {
|
|
512
|
+
promise.reject("CONCURRENT_REQUEST", error.localizedDescription)
|
|
513
|
+
return
|
|
538
514
|
}
|
|
539
|
-
|
|
515
|
+
if let failure = error as? GenerationWorkerFailure {
|
|
516
|
+
promise.reject("GENERATION_FAILED", failure.localizedDescription)
|
|
517
|
+
return
|
|
518
|
+
}
|
|
519
|
+
promise.reject(error)
|
|
540
520
|
}
|
|
541
521
|
|
|
522
|
+
#if canImport(FoundationModels)
|
|
542
523
|
@available(iOS 26, *)
|
|
543
|
-
private static func
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
524
|
+
private static func session(
|
|
525
|
+
system: String?,
|
|
526
|
+
messages: [TextMessageRequest]
|
|
527
|
+
) throws -> LanguageModelSession {
|
|
528
|
+
if !messages.isEmpty {
|
|
529
|
+
var entries: [Transcript.Entry] = []
|
|
530
|
+
if system?.isEmpty == false {
|
|
531
|
+
entries.append(.instructions(Transcript.Instructions(
|
|
532
|
+
segments: system.map { [.text(Transcript.TextSegment(content: $0))] } ?? [],
|
|
533
|
+
toolDefinitions: []
|
|
534
|
+
)))
|
|
535
|
+
}
|
|
536
|
+
for message in messages {
|
|
537
|
+
let segments: [Transcript.Segment] = [.text(Transcript.TextSegment(content: message.content))]
|
|
538
|
+
switch message.role {
|
|
539
|
+
case "user":
|
|
540
|
+
entries.append(.prompt(Transcript.Prompt(segments: segments)))
|
|
541
|
+
case "assistant":
|
|
542
|
+
entries.append(.response(Transcript.Response(assetIDs: [], segments: segments)))
|
|
543
|
+
default:
|
|
544
|
+
throw ModuleError.invalidMessage("Unsupported transcript role: \(message.role)")
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
return LanguageModelSession(transcript: Transcript(entries: entries))
|
|
548
|
+
}
|
|
549
|
+
if let system, !system.isEmpty {
|
|
550
|
+
return LanguageModelSession(instructions: system)
|
|
559
551
|
}
|
|
552
|
+
return LanguageModelSession()
|
|
560
553
|
}
|
|
561
554
|
|
|
562
555
|
@available(iOS 26, *)
|
|
@@ -568,131 +561,6 @@ public final class AIModule: Module {
|
|
|
568
561
|
)
|
|
569
562
|
}
|
|
570
563
|
|
|
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
564
|
@available(iOS 26, *)
|
|
697
565
|
private static func reject(_ error: Error, promise: Promise) {
|
|
698
566
|
if error is CancellationError {
|
|
@@ -716,55 +584,14 @@ public final class AIModule: Module {
|
|
|
716
584
|
}
|
|
717
585
|
return
|
|
718
586
|
}
|
|
719
|
-
if let toolError = error as? LanguageModelSession.ToolCallError {
|
|
720
|
-
promise.reject("TOOL_ERROR", toolError.localizedDescription)
|
|
721
|
-
return
|
|
722
|
-
}
|
|
723
|
-
|
|
724
587
|
promise.reject(error)
|
|
725
588
|
}
|
|
726
589
|
#endif
|
|
727
590
|
}
|
|
728
591
|
|
|
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
592
|
@MainActor
|
|
760
593
|
private final class TextGenerationCoordinator {
|
|
761
|
-
private struct PendingToolCall {
|
|
762
|
-
let generationID: String
|
|
763
|
-
let continuation: CheckedContinuation<String, Error>
|
|
764
|
-
}
|
|
765
|
-
|
|
766
594
|
private var tasks: [String: Task<Void, Never>] = [:]
|
|
767
|
-
private var toolCalls: [String: PendingToolCall] = [:]
|
|
768
595
|
|
|
769
596
|
func start(_ id: String, task: Task<Void, Never>) {
|
|
770
597
|
tasks[id]?.cancel()
|
|
@@ -773,67 +600,22 @@ private final class TextGenerationCoordinator {
|
|
|
773
600
|
|
|
774
601
|
func finish(_ id: String) {
|
|
775
602
|
tasks[id] = nil
|
|
776
|
-
failToolCalls(for: id, error: CancellationError())
|
|
777
603
|
}
|
|
778
604
|
|
|
779
605
|
func cancel(_ id: String) -> Bool {
|
|
780
606
|
guard let task = tasks.removeValue(forKey: id) else { return false }
|
|
781
607
|
task.cancel()
|
|
782
|
-
failToolCalls(for: id, error: CancellationError())
|
|
783
608
|
return true
|
|
784
609
|
}
|
|
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
610
|
}
|
|
826
611
|
|
|
827
612
|
private enum ModuleError: Error, LocalizedError {
|
|
828
|
-
case invalidSchema(String)
|
|
829
613
|
case invalidMessage(String)
|
|
830
614
|
case invalidModel(String)
|
|
831
615
|
case imageGeneration(String)
|
|
832
616
|
|
|
833
617
|
var errorDescription: String? {
|
|
834
618
|
switch self {
|
|
835
|
-
case .invalidSchema(let message):
|
|
836
|
-
return "Invalid schema: \(message)"
|
|
837
619
|
case .invalidMessage(let message), .invalidModel(let message), .imageGeneration(let message):
|
|
838
620
|
return message
|
|
839
621
|
}
|
|
@@ -849,6 +631,7 @@ private final class ImageGenerationServiceClient {
|
|
|
849
631
|
}
|
|
850
632
|
private var connection: NSXPCConnection?
|
|
851
633
|
private var pendingRun: PendingRun?
|
|
634
|
+
private var pendingTextRun: PendingTextRun?
|
|
852
635
|
private let eventReceiver = GenerationServiceEventReceiver()
|
|
853
636
|
|
|
854
637
|
private final class PendingRun {
|
|
@@ -864,13 +647,29 @@ private final class ImageGenerationServiceClient {
|
|
|
864
647
|
}
|
|
865
648
|
}
|
|
866
649
|
|
|
650
|
+
private final class PendingTextRun {
|
|
651
|
+
let id: String
|
|
652
|
+
let continuation: CheckedContinuation<BonsaiTextGenerationEvent, Error>
|
|
653
|
+
let onUpdate: (BonsaiTextGenerationEvent) -> Void
|
|
654
|
+
|
|
655
|
+
init(
|
|
656
|
+
id: String,
|
|
657
|
+
continuation: CheckedContinuation<BonsaiTextGenerationEvent, Error>,
|
|
658
|
+
onUpdate: @escaping (BonsaiTextGenerationEvent) -> Void
|
|
659
|
+
) {
|
|
660
|
+
self.id = id
|
|
661
|
+
self.continuation = continuation
|
|
662
|
+
self.onUpdate = onUpdate
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
867
666
|
func run(
|
|
868
667
|
request: GenerationWorkerRequest,
|
|
869
668
|
onProgress: @escaping (GenerationWorkerEvent) -> Void = { _ in }
|
|
870
669
|
) async throws -> GenerationWorkerEvent {
|
|
871
670
|
let terminalEvent: GenerationWorkerEvent = try await withCheckedThrowingContinuation { continuation in
|
|
872
671
|
do {
|
|
873
|
-
guard pendingRun == nil else {
|
|
672
|
+
guard pendingRun == nil, pendingTextRun == nil else {
|
|
874
673
|
continuation.resume(throwing: GenerationServiceClientError.concurrentRequest)
|
|
875
674
|
return
|
|
876
675
|
}
|
|
@@ -905,6 +704,71 @@ private final class ImageGenerationServiceClient {
|
|
|
905
704
|
return terminalEvent
|
|
906
705
|
}
|
|
907
706
|
|
|
707
|
+
func runText(
|
|
708
|
+
request: BonsaiTextGenerationRequest,
|
|
709
|
+
onUpdate: @escaping (BonsaiTextGenerationEvent) -> Void = { _ in }
|
|
710
|
+
) async throws -> BonsaiTextGenerationEvent {
|
|
711
|
+
let terminalEvent: BonsaiTextGenerationEvent = try await withCheckedThrowingContinuation { continuation in
|
|
712
|
+
do {
|
|
713
|
+
guard pendingRun == nil, pendingTextRun == nil else {
|
|
714
|
+
continuation.resume(throwing: GenerationServiceClientError.concurrentRequest)
|
|
715
|
+
return
|
|
716
|
+
}
|
|
717
|
+
eventReceiver.onTextEvent = { [weak self] event in
|
|
718
|
+
self?.deliverText(event)
|
|
719
|
+
}
|
|
720
|
+
pendingTextRun = PendingTextRun(
|
|
721
|
+
id: request.id,
|
|
722
|
+
continuation: continuation,
|
|
723
|
+
onUpdate: onUpdate
|
|
724
|
+
)
|
|
725
|
+
let requestData = try JSONEncoder().encode(request)
|
|
726
|
+
let service = try remoteProxy(errorHandler: { [weak self] error in
|
|
727
|
+
Task { @MainActor in
|
|
728
|
+
self?.failPendingTextRun(Self.mapConnectionError(error))
|
|
729
|
+
}
|
|
730
|
+
})
|
|
731
|
+
service.startTextGeneration(requestData: requestData) { [weak self] accepted in
|
|
732
|
+
Task { @MainActor in
|
|
733
|
+
guard accepted else {
|
|
734
|
+
self?.failPendingTextRun(GenerationServiceClientError.concurrentRequest)
|
|
735
|
+
return
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
} catch {
|
|
740
|
+
pendingTextRun = nil
|
|
741
|
+
eventReceiver.onTextEvent = nil
|
|
742
|
+
continuation.resume(throwing: error)
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
switch terminalEvent.type {
|
|
747
|
+
case .completed:
|
|
748
|
+
return terminalEvent
|
|
749
|
+
case .canceled:
|
|
750
|
+
throw CancellationError()
|
|
751
|
+
case .failed:
|
|
752
|
+
throw GenerationWorkerFailure(message: terminalEvent.message ?? "Text generation failed.")
|
|
753
|
+
case .update:
|
|
754
|
+
throw GenerationServiceClientError.invalidResponse
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
func cancelText(id: String) async throws -> Bool {
|
|
759
|
+
guard pendingTextRun?.id == id else { return false }
|
|
760
|
+
let service = try remoteProxy(errorHandler: { [weak self] error in
|
|
761
|
+
Task { @MainActor in
|
|
762
|
+
self?.failPendingTextRun(Self.mapConnectionError(error))
|
|
763
|
+
}
|
|
764
|
+
})
|
|
765
|
+
return try await withCheckedThrowingContinuation { continuation in
|
|
766
|
+
service.cancelTextGeneration(id: id) { canceled in
|
|
767
|
+
continuation.resume(returning: canceled)
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
|
|
908
772
|
func cancel() async throws -> Bool {
|
|
909
773
|
guard pendingRun != nil else {
|
|
910
774
|
return false
|
|
@@ -940,7 +804,7 @@ private final class ImageGenerationServiceClient {
|
|
|
940
804
|
}
|
|
941
805
|
|
|
942
806
|
func unload() async throws -> Bool {
|
|
943
|
-
guard pendingRun == nil else { return false }
|
|
807
|
+
guard pendingRun == nil, pendingTextRun == nil else { return false }
|
|
944
808
|
let service = try remoteProxy(errorHandler: { _ in })
|
|
945
809
|
let unloaded = try await withCheckedThrowingContinuation { continuation in
|
|
946
810
|
service.shutdown { accepted in
|
|
@@ -1025,6 +889,17 @@ private final class ImageGenerationServiceClient {
|
|
|
1025
889
|
}
|
|
1026
890
|
}
|
|
1027
891
|
|
|
892
|
+
private func deliverText(_ event: BonsaiTextGenerationEvent) {
|
|
893
|
+
guard let pendingTextRun, event.id == pendingTextRun.id else { return }
|
|
894
|
+
if event.type == .update {
|
|
895
|
+
pendingTextRun.onUpdate(event)
|
|
896
|
+
return
|
|
897
|
+
}
|
|
898
|
+
self.pendingTextRun = nil
|
|
899
|
+
eventReceiver.onTextEvent = nil
|
|
900
|
+
pendingTextRun.continuation.resume(returning: event)
|
|
901
|
+
}
|
|
902
|
+
|
|
1028
903
|
private func failPendingRun(_ error: Error) {
|
|
1029
904
|
guard let pendingRun else { return }
|
|
1030
905
|
self.pendingRun = nil
|
|
@@ -1035,9 +910,17 @@ private final class ImageGenerationServiceClient {
|
|
|
1035
910
|
pendingRun.continuation.resume(throwing: error)
|
|
1036
911
|
}
|
|
1037
912
|
|
|
913
|
+
private func failPendingTextRun(_ error: Error) {
|
|
914
|
+
guard let pendingTextRun else { return }
|
|
915
|
+
self.pendingTextRun = nil
|
|
916
|
+
eventReceiver.onTextEvent = nil
|
|
917
|
+
pendingTextRun.continuation.resume(throwing: error)
|
|
918
|
+
}
|
|
919
|
+
|
|
1038
920
|
private func handleConnectionLost() {
|
|
1039
921
|
connection = nil
|
|
1040
922
|
failPendingRun(GenerationServiceClientError.serviceInterrupted)
|
|
923
|
+
failPendingTextRun(GenerationServiceClientError.serviceInterrupted)
|
|
1041
924
|
}
|
|
1042
925
|
|
|
1043
926
|
private func invalidate() {
|
|
@@ -1150,6 +1033,7 @@ private enum GenerationServiceClientError: LocalizedError, Equatable {
|
|
|
1150
1033
|
|
|
1151
1034
|
private final class GenerationServiceEventReceiver: NSObject, GenerationServiceClientProtocol {
|
|
1152
1035
|
@MainActor var onEvent: ((GenerationWorkerEvent) -> Void)?
|
|
1036
|
+
@MainActor var onTextEvent: ((BonsaiTextGenerationEvent) -> Void)?
|
|
1153
1037
|
|
|
1154
1038
|
func generationWorkerDidEmit(eventData: Data) {
|
|
1155
1039
|
guard let event = try? JSONDecoder().decode(GenerationWorkerEvent.self, from: eventData) else { return }
|
|
@@ -1157,29 +1041,43 @@ private final class GenerationServiceEventReceiver: NSObject, GenerationServiceC
|
|
|
1157
1041
|
onEvent?(event)
|
|
1158
1042
|
}
|
|
1159
1043
|
}
|
|
1044
|
+
|
|
1045
|
+
|
|
1046
|
+
func textGenerationWorkerDidEmit(eventData: Data) {
|
|
1047
|
+
guard let event = try? JSONDecoder().decode(BonsaiTextGenerationEvent.self, from: eventData) else { return }
|
|
1048
|
+
Task { @MainActor in
|
|
1049
|
+
onTextEvent?(event)
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1160
1052
|
}
|
|
1161
1053
|
|
|
1162
1054
|
private struct TextGenerationRequest: Record {
|
|
1163
1055
|
@Field var id: String?
|
|
1164
1056
|
@Field var system: String?
|
|
1165
1057
|
@Field var prompt: String = ""
|
|
1166
|
-
@Field var
|
|
1058
|
+
@Field var messages: [TextMessageRequest] = []
|
|
1167
1059
|
@Field var temperature: Double?
|
|
1168
1060
|
@Field var maxTokens: Int?
|
|
1169
|
-
@Field var tools: [ToolDefinitionRequest] = []
|
|
1170
1061
|
}
|
|
1171
1062
|
|
|
1172
|
-
private struct
|
|
1173
|
-
@Field var
|
|
1174
|
-
@Field var
|
|
1175
|
-
@Field var inputSchema: [String: Any] = [:]
|
|
1063
|
+
private struct TextMessageRequest: Record {
|
|
1064
|
+
@Field var role: String = ""
|
|
1065
|
+
@Field var content: String = ""
|
|
1176
1066
|
}
|
|
1177
1067
|
|
|
1178
|
-
private struct
|
|
1179
|
-
@Field var
|
|
1180
|
-
@Field var
|
|
1181
|
-
@Field var
|
|
1182
|
-
@Field var
|
|
1068
|
+
private struct LocalTextGenerationRequest: Record {
|
|
1069
|
+
@Field var id: String = ""
|
|
1070
|
+
@Field var model: String = ""
|
|
1071
|
+
@Field var modelPath: String = ""
|
|
1072
|
+
@Field var inputIds: [Int] = []
|
|
1073
|
+
@Field var endTokenIds: [Int] = []
|
|
1074
|
+
@Field var maxTokens: Int?
|
|
1075
|
+
@Field var temperature: Double?
|
|
1076
|
+
@Field var topP: Double?
|
|
1077
|
+
@Field var topK: Int?
|
|
1078
|
+
@Field var repetitionPenalty: Double?
|
|
1079
|
+
@Field var repetitionContextSize: Int?
|
|
1080
|
+
@Field var seed: Int?
|
|
1183
1081
|
}
|
|
1184
1082
|
|
|
1185
1083
|
private struct ImageGenerationRequest: Record {
|