@swiftbrowser/native 0.18.2 → 0.19.0

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 (73) hide show
  1. package/README.md +19 -0
  2. package/bin/swiftbrowser-native.mjs +47 -0
  3. package/dist/bridge.d.ts +34 -7
  4. package/dist/bridge.d.ts.map +1 -1
  5. package/dist/bridge.js.map +1 -1
  6. package/dist/encoding.d.ts +13 -0
  7. package/dist/encoding.d.ts.map +1 -1
  8. package/dist/encoding.js +24 -0
  9. package/dist/encoding.js.map +1 -1
  10. package/dist/fetch.d.ts +10 -4
  11. package/dist/fetch.d.ts.map +1 -1
  12. package/dist/fetch.js +19 -11
  13. package/dist/fetch.js.map +1 -1
  14. package/dist/hostRequests.d.ts +22 -6
  15. package/dist/hostRequests.d.ts.map +1 -1
  16. package/dist/hostRequests.js +65 -13
  17. package/dist/hostRequests.js.map +1 -1
  18. package/dist/ios.d.ts +79 -0
  19. package/dist/ios.d.ts.map +1 -0
  20. package/dist/ios.js +181 -0
  21. package/dist/ios.js.map +1 -0
  22. package/dist/runtime.d.ts.map +1 -1
  23. package/dist/runtime.js +16 -7
  24. package/dist/runtime.js.map +1 -1
  25. package/dist/subtle.d.ts +1 -1
  26. package/dist/subtle.d.ts.map +1 -1
  27. package/dist/subtle.js +9 -5
  28. package/dist/subtle.js.map +1 -1
  29. package/dist/webSocket.d.ts +12 -5
  30. package/dist/webSocket.d.ts.map +1 -1
  31. package/dist/webSocket.js +24 -10
  32. package/dist/webSocket.js.map +1 -1
  33. package/package.json +7 -3
  34. package/src/bridge.ts +30 -7
  35. package/src/encoding.ts +22 -0
  36. package/src/fetch.ts +18 -13
  37. package/src/hostRequests.ts +72 -19
  38. package/src/ios.ts +253 -0
  39. package/src/runtime.ts +23 -8
  40. package/src/subtle.ts +10 -6
  41. package/src/webSocket.ts +27 -12
  42. package/swift/Packages/SwiftBrowserDSP/Package.swift +31 -0
  43. package/swift/Packages/SwiftBrowserDSP/README.md +42 -0
  44. package/swift/Packages/SwiftBrowserDSP/Sources/SwiftBrowserDSP/ProgramNode.swift +203 -0
  45. package/swift/Packages/SwiftBrowserDSP/Sources/SwiftBrowserDSPCore/Machine.swift +523 -0
  46. package/swift/Packages/SwiftBrowserDSP/Sources/SwiftBrowserDSPCore/Program.swift +188 -0
  47. package/swift/Packages/SwiftBrowserLocalModel/Package.swift +35 -0
  48. package/swift/Packages/SwiftBrowserLocalModel/README.md +66 -0
  49. package/swift/Packages/SwiftBrowserLocalModel/Sources/SwiftBrowserLocalModel/LlamaEngine.swift +223 -0
  50. package/swift/Packages/SwiftBrowserLocalModel/Sources/SwiftBrowserLocalModel/LocalLanguageModel.swift +409 -0
  51. package/swift/Sources/SwiftBrowserNative/Host/AudioEngineBridge.swift +921 -0
  52. package/swift/Sources/SwiftBrowserNative/Host/AudioEngineNodes.swift +70 -0
  53. package/swift/Sources/SwiftBrowserNative/Host/FilesBridge.swift +3 -3
  54. package/swift/Sources/SwiftBrowserNative/Host/HostBridge.swift +106 -28
  55. package/swift/Sources/SwiftBrowserNative/Host/HostRequestBridge.swift +43 -13
  56. package/swift/Sources/SwiftBrowserNative/Host/JSHost.swift +116 -0
  57. package/swift/Sources/SwiftBrowserNative/Host/JavaScriptCoreHost.swift +141 -2
  58. package/swift/Sources/SwiftBrowserNative/Host/LanguageModelBridge.swift +511 -0
  59. package/swift/Sources/SwiftBrowserNative/Host/LanguageModelProvider.swift +195 -0
  60. package/swift/Sources/SwiftBrowserNative/Host/WebSocketBridge.swift +24 -11
  61. package/swift/Sources/SwiftBrowserNative/Host/WorkerHost.swift +3 -2
  62. package/swift/Sources/SwiftBrowserNative/Views/ElementView.swift +1 -0
  63. package/swift/Sources/SwiftBrowserNative/Views/Kinds/Controls.swift +18 -1
  64. package/swift/Sources/SwiftBrowserNative/Views/Kinds/Lists.swift +41 -1
  65. package/swift/Sources/SwiftBrowserNative/Views/Kinds/More.swift +88 -17
  66. package/swift/Sources/SwiftBrowserNative/Views/Kinds/Navigation.swift +81 -20
  67. package/swift/Sources/SwiftBrowserNative/Views/Kinds/PinchTracker.swift +200 -0
  68. package/swift/Sources/SwiftBrowserNative/Views/Kinds/PopTap.swift +100 -0
  69. package/swift/Sources/SwiftBrowserNative/Views/Kinds/Stacks.swift +3 -2
  70. package/swift/Sources/SwiftBrowserNative/Views/StyleModifier.swift +4 -0
  71. package/swift/Sources/SwiftBrowserProtocol/Props.swift +26 -0
  72. package/swift/Sources/SwiftBrowserTree/OpTree.swift +25 -0
  73. package/wasm/SwiftBrowserNative.wasm +2 -2
@@ -0,0 +1,188 @@
1
+ import Foundation
2
+
3
+ /// A compiled DSP program: the JSON @swiftbrowser/dsp's compiler writes
4
+ /// (packages/dsp/src/program.ts, which documents the format). The machine
5
+ /// keeps a register file of `registers` registers of 16 lanes, a lane per
6
+ /// voice; `block` runs once per block of up to 128 frames and `sample` once
7
+ /// per frame, each a flat list of instructions: an opcode, a destination
8
+ /// register, the opcode's operands and, for a stateful builtin, its instance.
9
+ public struct DSPProgram: Sendable, Equatable {
10
+ public struct Parameter: Sendable, Equatable {
11
+ public let name: String
12
+ public let register: Int
13
+ public let defaultValue: Float
14
+ public let min: Float
15
+ public let max: Float
16
+ }
17
+
18
+ public struct State: Sendable, Equatable {
19
+ public let name: String
20
+ public let register: Int
21
+ public let initial: Float
22
+ }
23
+
24
+ public let voices: Int
25
+ public let registers: Int
26
+ public let constants: [(register: Int, value: Float)]
27
+ public let parameters: [Parameter]
28
+ public let state: [State]
29
+ public let instances: [DSPBuiltin]
30
+ public let block: [Int32]
31
+ public let sample: [Int32]
32
+ public let output: Int
33
+
34
+ public static func == (a: DSPProgram, b: DSPProgram) -> Bool {
35
+ a.voices == b.voices && a.registers == b.registers && a.constants.map(\.register) == b.constants.map(\.register)
36
+ && a.constants.map(\.value) == b.constants.map(\.value) && a.parameters == b.parameters && a.state == b.state
37
+ && a.instances == b.instances && a.block == b.block && a.sample == b.sample && a.output == b.output
38
+ }
39
+
40
+ public static let format = "swiftbrowser-dsp"
41
+ public static let version = 1
42
+ public static let lanes = 16
43
+ public static let blockFrames = 128
44
+ /// The machine's own registers: `sampleRate`, then a voice's `freq`, `gate`, `velocity` and `note`.
45
+ public static let machineRegisters = 5
46
+
47
+ /// The program the JSON describes, checked: every register, operand and instance in range.
48
+ public init(json: String) throws {
49
+ guard let data = json.data(using: .utf8) else { throw DSPProgramError("not UTF-8") }
50
+ try self.init(data: data)
51
+ }
52
+
53
+ public init(data: Data) throws {
54
+ let parsed: Any
55
+ do {
56
+ parsed = try JSONSerialization.jsonObject(with: data)
57
+ } catch {
58
+ throw DSPProgramError("not JSON (\(error.localizedDescription))")
59
+ }
60
+ try self.init(object: parsed)
61
+ }
62
+
63
+ /// From a decoded JSON object (`JSONSerialization`'s).
64
+ public init(object: Any) throws {
65
+ guard let root = object as? [String: Any] else { throw DSPProgramError("not an object") }
66
+ guard root["format"] as? String == Self.format else { throw DSPProgramError("format is not \(Self.format)") }
67
+ guard Self.int(root["version"]) == Self.version else { throw DSPProgramError("version is not \(Self.version)") }
68
+ guard let voices = Self.int(root["voices"]), (1...Self.lanes).contains(voices) else { throw DSPProgramError("voices must be 1 … \(Self.lanes)") }
69
+ guard let registers = Self.int(root["registers"]), (Self.machineRegisters...65_536).contains(registers) else { throw DSPProgramError("registers out of range") }
70
+ func register(_ value: Any?, _ what: String) throws -> Int {
71
+ guard let r = Self.int(value), r >= 0, r < registers else { throw DSPProgramError("\(what): register out of range") }
72
+ return r
73
+ }
74
+ func number(_ value: Any?, _ what: String) throws -> Float {
75
+ guard let n = (value as? NSNumber)?.doubleValue, n.isFinite || n.isInfinite else { throw DSPProgramError("\(what) must be a number") }
76
+ return Float(n)
77
+ }
78
+ guard let instanceNames = root["instances"] as? [String] else { throw DSPProgramError("instances must be an array of names") }
79
+ let instances = try instanceNames.map { name in
80
+ guard let builtin = DSPBuiltin(rawValue: name) else { throw DSPProgramError("unknown builtin \(name)") }
81
+ return builtin
82
+ }
83
+ guard let constants = root["constants"] as? [[Any]] else { throw DSPProgramError("constants missing") }
84
+ let parsedConstants: [(register: Int, value: Float)] = try constants.map { entry in
85
+ guard entry.count == 2 else { throw DSPProgramError("a constant is not [register, value]") }
86
+ return (try register(entry[0], "constant"), try number(entry[1], "a constant"))
87
+ }
88
+ guard let parameters = root["parameters"] as? [[String: Any]] else { throw DSPProgramError("parameters missing") }
89
+ let parsedParameters = try parameters.map { p -> Parameter in
90
+ guard let name = p["name"] as? String else { throw DSPProgramError("a parameter has no name") }
91
+ let parameter = Parameter(
92
+ name: name, register: try register(p["register"], "parameter \(name)"),
93
+ defaultValue: try number(p["default"], "parameter \(name)'s default"),
94
+ min: try number(p["min"], "parameter \(name)'s min"), max: try number(p["max"], "parameter \(name)'s max")
95
+ )
96
+ guard parameter.min <= parameter.max, [parameter.defaultValue, parameter.min, parameter.max].allSatisfy(\.isFinite) else {
97
+ throw DSPProgramError("parameter \(name): its range is not numbers from min to max")
98
+ }
99
+ return parameter
100
+ }
101
+ guard let state = root["state"] as? [[String: Any]] else { throw DSPProgramError("state missing") }
102
+ let parsedState = try state.map { s -> State in
103
+ let name = s["name"] as? String ?? "state"
104
+ return State(name: name, register: try register(s["register"], "state \(name)"), initial: try number(s["initial"], "state \(name)'s initial value"))
105
+ }
106
+ let output = try register(root["output"], "output")
107
+ func code(_ key: String) throws -> [Int32] {
108
+ guard let words = root[key] as? [Any] else { throw DSPProgramError("\(key) must be an array") }
109
+ let code = try words.map { word -> Int32 in
110
+ guard let n = Self.int(word), n >= Int(Int32.min), n <= Int(Int32.max) else { throw DSPProgramError("\(key): a word is not a whole number") }
111
+ return Int32(n)
112
+ }
113
+ var pc = 0
114
+ while pc < code.count {
115
+ guard let op = DSPOpcode(rawValue: code[pc]) else { throw DSPProgramError("\(key): unknown opcode \(code[pc]) at \(pc)") }
116
+ let length = 2 + op.arity + (op.builtin == nil ? 0 : 1)
117
+ guard pc + length <= code.count else { throw DSPProgramError("\(key): \(op) at \(pc) is cut off") }
118
+ for i in 1..<(2 + op.arity) { _ = try register(Int(code[pc + i]), "\(key) \(op) at \(pc)") }
119
+ if let builtin = op.builtin {
120
+ let instance = Int(code[pc + length - 1])
121
+ guard instances.indices.contains(instance), instances[instance] == builtin else {
122
+ throw DSPProgramError("\(key): \(op) at \(pc) names instance \(instance), not a \(builtin)")
123
+ }
124
+ }
125
+ pc += length
126
+ }
127
+ return code
128
+ }
129
+ self.voices = voices
130
+ self.registers = registers
131
+ self.constants = parsedConstants
132
+ self.parameters = parsedParameters
133
+ self.state = parsedState
134
+ self.instances = instances
135
+ self.block = try code("block")
136
+ self.sample = try code("sample")
137
+ self.output = output
138
+ }
139
+
140
+ private static func int(_ value: Any?) -> Int? {
141
+ guard let number = value as? NSNumber else { return nil }
142
+ let double = number.doubleValue
143
+ guard double.rounded() == double, abs(double) < 1e12 else { return nil }
144
+ return Int(double)
145
+ }
146
+ }
147
+
148
+ public struct DSPProgramError: Error, CustomStringConvertible, Equatable {
149
+ public let description: String
150
+ public init(_ detail: String) { description = "invalidProgram: \(detail)" }
151
+ }
152
+
153
+ /// The stateful builtins, by the name a program's `instances` gives them.
154
+ public enum DSPBuiltin: String, Sendable, CaseIterable {
155
+ case phasor, sine, saw, square, triangle, noise, adsr, lowpass, highpass, bandpass
156
+
157
+ /// Words of state per lane (program.ts BUILTIN_STATE).
158
+ var words: Int {
159
+ switch self {
160
+ case .phasor, .sine, .saw, .square, .triangle, .noise: 1
161
+ case .adsr, .lowpass, .highpass, .bandpass: 8
162
+ }
163
+ }
164
+ }
165
+
166
+ /// The opcodes, v1: the numbers are the wire format (program.ts OPCODES).
167
+ public enum DSPOpcode: Int32, Sendable, CaseIterable {
168
+ case mov = 0, add, sub, mul, div, mod, neg, min, max, abs, floor, ceil, round, sqrt, sin, cos, tan, exp, log, pow, tanh
169
+ case lt = 21, le, gt, ge, eq, ne, and, or, not, select, clamp, mix, wrap, sign, atan
170
+ case phasor = 40, sine, saw, square, triangle, noise, adsr, lowpass, highpass, bandpass
171
+
172
+ /// Operands after the destination.
173
+ public var arity: Int {
174
+ switch self {
175
+ case .noise: 0
176
+ case .mov, .neg, .abs, .floor, .ceil, .round, .sqrt, .sin, .cos, .tan, .exp, .log, .tanh, .not, .wrap, .sign, .atan,
177
+ .phasor, .sine, .saw, .triangle: 1
178
+ case .select, .clamp, .mix, .lowpass, .highpass, .bandpass: 3
179
+ case .adsr: 5
180
+ default: 2
181
+ }
182
+ }
183
+
184
+ /// The builtin whose state it keeps, for a stateful one.
185
+ public var builtin: DSPBuiltin? {
186
+ rawValue >= 40 ? DSPBuiltin.allCases[Int(rawValue) - 40] : nil
187
+ }
188
+ }
@@ -0,0 +1,35 @@
1
+ // swift-tools-version: 6.0
2
+ import PackageDescription
3
+
4
+ // SwiftBrowserLocalModel: an open language model on the device, through
5
+ // llama.cpp, for the `LanguageModelSession`s of a SwiftBrowser Native app
6
+ // where Apple Intelligence cannot answer (an iPhone before the 15 Pro, Apple
7
+ // Intelligence off, a system before iOS 26). A package of its own, so only an
8
+ // app that adds it fetches, builds and links llama.cpp; it installs itself as
9
+ // `LanguageModels.fallback` (SwiftBrowserNative, LanguageModelProvider.swift).
10
+ //
11
+ // llama.cpp is its release XCFramework, b10456: the newest whose release
12
+ // carries the iOS Simulator beside devices and macOS (later ones ship devices
13
+ // and macOS only). SwiftPM fetches it once (286 MB, tvOS and visionOS
14
+ // included) and caches it; an iPhone app links about 6 MB of it.
15
+ let package = Package(
16
+ name: "SwiftBrowserLocalModel",
17
+ platforms: [.iOS(.v17), .macOS(.v14)],
18
+ products: [
19
+ .library(name: "SwiftBrowserLocalModel", targets: ["SwiftBrowserLocalModel"]),
20
+ ],
21
+ dependencies: [
22
+ .package(path: "../.."),
23
+ ],
24
+ targets: [
25
+ .binaryTarget(
26
+ name: "llama",
27
+ url: "https://github.com/ggml-org/llama.cpp/releases/download/b10456/llama-b10456-xcframework.zip",
28
+ checksum: "0223bedd0a01232399d943dcb72bc227882bc90df98e29d7a92343531a88cc02"
29
+ ),
30
+ .target(
31
+ name: "SwiftBrowserLocalModel",
32
+ dependencies: [.product(name: "SwiftBrowserNative", package: "swift"), "llama"]
33
+ ),
34
+ ]
35
+ )
@@ -0,0 +1,66 @@
1
+ # SwiftBrowserLocalModel
2
+
3
+ An open language model on the device, run with
4
+ [llama.cpp](https://github.com/ggml-org/llama.cpp), for a SwiftBrowser Native
5
+ app's `LanguageModelSession`s where Apple Intelligence cannot answer: an
6
+ iPhone before the 15 Pro, Apple Intelligence turned off, a system before
7
+ iOS 26. Where Apple Intelligence answers, it still does.
8
+
9
+ It is a package of its own so that only an app that adds it fetches, builds
10
+ and links llama.cpp. @swiftbrowser/native ships it beside the core
11
+ (`node_modules/@swiftbrowser/native/swift/Packages/SwiftBrowserLocalModel`;
12
+ it depends on `SwiftBrowserNative` at `../..`, here and there). Link it into
13
+ the app's Xcode project with
14
+
15
+ ```sh
16
+ npx swiftbrowser native add local-model # or native init --with local-model
17
+ ```
18
+
19
+ and install a model at launch:
20
+
21
+ ```swift
22
+ import SwiftBrowserLocalModel
23
+
24
+ init() {
25
+ LocalLanguageModel.install([.llama3_2_1B, .qwen2_5_1_5B, .llama3_2_3B])
26
+ }
27
+ ```
28
+
29
+ With several, the first is the default and availability lists them as
30
+ `models` for the viewer to choose from (`selectModel(id)`), the choice
31
+ remembered across launches; each downloads on its own.
32
+
33
+ On the JavaScript side, `useSystemLanguageModel()` reports `modelNotReady`
34
+ with `download: { size, downloaded: false }` until the viewer asks for the
35
+ model: show the size and call `SystemLanguageModel.default.download()` from
36
+ a button. The download's `progress` follows, then `available`, and
37
+ `LanguageModelSession` answers with it. `deleteDownload()` stops a download
38
+ or takes the model off the device (the Showcase's Chat has both).
39
+
40
+ - **Tools** a session is given are offered in its instructions, and a reply
41
+ that is just `{"tool": name, "arguments": {...}}` is run as a call, its
42
+ result handed back for the model to go on. Small models call tools less
43
+ reliably than Apple Intelligence does; Qwen 2.5 1.5B is the steadiest here.
44
+ - **The models**: `.llama3_2_1B` (808 MB, fastest), `.qwen2_5_1_5B` (1.1 GB,
45
+ Apache 2.0, best with tools), `.llama3_2_3B` (2.0 GB, most capable; a phone
46
+ with 6 GB of memory runs it near its limit).
47
+ - **The model** is a GGUF file, fetched when the app calls `download()`, kept
48
+ in Application Support's
49
+ `SwiftBrowserLocalModel` folder and excluded from backups. `.llama3_2_1B`
50
+ is Meta's Llama 3.2 1B Instruct at 4 bits (808 MB; Meta's Llama 3.2
51
+ Community License applies); any other is a `LocalLanguageModel.Model`
52
+ with its URL, size and context length. `allowsCellularAccess: false`
53
+ waits for Wi-Fi.
54
+ - **Running it**: loaded once on the first prompt or `prewarm()`, on the GPU
55
+ with Metal, on a queue of its own. A session keeps its messages, formatted
56
+ with the model's chat template (ChatML for a model without one); the
57
+ context keeps the tokens of the last conversation, so a prompt that
58
+ continues it decodes only what is new. A conversation longer than the
59
+ context fails as `exceededContextWindowSize`.
60
+ - **Size**: llama.cpp is its release XCFramework, b10456, the newest whose
61
+ release has the iOS Simulator beside devices and macOS. SwiftPM downloads
62
+ it once (286 MB, with tvOS and visionOS) and caches it; an iPhone app
63
+ links about 6 MB.
64
+
65
+ The tests (`swift test`, macOS) run llama.cpp on the CPU with a 1.2 MB toy
66
+ model they download.
@@ -0,0 +1,223 @@
1
+ import Foundation
2
+ import llama
3
+ import SwiftBrowserNative
4
+
5
+ /// A model loaded with llama.cpp and the one context it answers in, used
6
+ /// from a serial queue of its own (every call into llama.cpp blocks). The
7
+ /// context keeps the tokens of the last conversation it saw, so a prompt that
8
+ /// continues it decodes only what is new.
9
+ final class LlamaEngine: @unchecked Sendable {
10
+ struct Message: Sendable, Equatable {
11
+ let role: String
12
+ let content: String
13
+ }
14
+
15
+ let contextLength: Int
16
+ private let queue = DispatchQueue(label: "dev.swiftbrowser.localmodel.llama", qos: .userInitiated)
17
+ private let model: OpaquePointer
18
+ private let context: OpaquePointer
19
+ private let vocab: OpaquePointer
20
+ /// The model's chat template, or ChatML for a model without one.
21
+ private let template: String
22
+ /// The tokens the context holds, in order.
23
+ private var cached: [llama_token] = []
24
+
25
+ /// llama.cpp's backends, started once, with its logging silenced.
26
+ private static let backend: Void = {
27
+ llama_backend_init()
28
+ llama_log_set({ _, _, _ in }, nil)
29
+ }()
30
+
31
+ /// Loads the model at `path`. Blocking: call it off the main thread.
32
+ init(path: String, contextLength: Int, gpuLayers: Int32) throws {
33
+ _ = Self.backend
34
+ var modelParams = llama_model_default_params()
35
+ modelParams.n_gpu_layers = gpuLayers
36
+ guard let model = llama_model_load_from_file(path, modelParams) else {
37
+ throw LanguageModelError(.unavailable, "the model could not be loaded")
38
+ }
39
+ var contextParams = llama_context_default_params()
40
+ contextParams.n_ctx = UInt32(contextLength)
41
+ contextParams.n_batch = 512
42
+ let threads = Int32(max(1, min(8, ProcessInfo.processInfo.activeProcessorCount - 2)))
43
+ contextParams.n_threads = threads
44
+ contextParams.n_threads_batch = threads
45
+ guard let context = llama_init_from_model(model, contextParams) else {
46
+ llama_model_free(model)
47
+ throw LanguageModelError(.unavailable, "the model's context could not be made")
48
+ }
49
+ guard let vocab = llama_model_get_vocab(model) else {
50
+ llama_free(context)
51
+ llama_model_free(model)
52
+ throw LanguageModelError(.unavailable, "the model has no vocabulary")
53
+ }
54
+ self.model = model
55
+ self.context = context
56
+ self.vocab = vocab
57
+ self.contextLength = Int(llama_n_ctx(context))
58
+ template = llama_model_chat_template(model, nil).map { String(cString: $0) } ?? "chatml"
59
+ }
60
+
61
+ /// The role a tool's result goes back to the model as: Llama 3's `ipython`, else `tool`.
62
+ var toolRole: String {
63
+ template.contains("<|start_header_id|>") ? "ipython" : "tool"
64
+ }
65
+
66
+ deinit {
67
+ llama_free(context)
68
+ llama_model_free(model)
69
+ }
70
+
71
+ /// The reply to the last message of `messages`, calling `onText` with the
72
+ /// reply so far as it grows; stops early once `cancelled` is set, with
73
+ /// what came by then.
74
+ func generate(
75
+ _ messages: [Message],
76
+ options: LanguageModelOptions,
77
+ cancelled: CancelFlag,
78
+ onText: @escaping @Sendable (String) -> Void
79
+ ) async throws -> String {
80
+ try await withCheckedThrowingContinuation { continuation in
81
+ queue.async {
82
+ do {
83
+ continuation.resume(returning: try self.generateNow(messages, options: options, cancelled: cancelled, onText: onText))
84
+ } catch {
85
+ continuation.resume(throwing: error)
86
+ }
87
+ }
88
+ }
89
+ }
90
+
91
+ private func generateNow(_ messages: [Message], options: LanguageModelOptions, cancelled: CancelFlag, onText: (String) -> Void) throws -> String {
92
+ let tokens = tokenize(try format(messages))
93
+ guard tokens.count + 16 < contextLength else {
94
+ throw LanguageModelError(.context, "the conversation is longer than the model's \(contextLength) tokens")
95
+ }
96
+ // Keep what the context already holds of this conversation; decode the rest.
97
+ var kept = 0
98
+ while kept < min(cached.count, tokens.count), cached[kept] == tokens[kept] { kept += 1 }
99
+ if kept == tokens.count { kept -= 1 }
100
+ let memory = llama_get_memory(context)
101
+ if !llama_memory_seq_rm(memory, 0, llama_pos(kept), -1) {
102
+ llama_memory_clear(memory, true)
103
+ kept = 0
104
+ }
105
+ cached.removeSubrange(min(kept, cached.count)...)
106
+ var start = kept
107
+ while start < tokens.count {
108
+ if cancelled.isCancelled { return "" }
109
+ let end = min(start + 512, tokens.count)
110
+ try decode(Array(tokens[start..<end]))
111
+ cached.append(contentsOf: tokens[start..<end])
112
+ start = end
113
+ }
114
+
115
+ let sampler = llama_sampler_chain_init(llama_sampler_chain_default_params())
116
+ defer { llama_sampler_free(sampler) }
117
+ if options.greedy {
118
+ llama_sampler_chain_add(sampler, llama_sampler_init_greedy())
119
+ } else {
120
+ llama_sampler_chain_add(sampler, llama_sampler_init_top_k(40))
121
+ llama_sampler_chain_add(sampler, llama_sampler_init_top_p(0.9, 1))
122
+ llama_sampler_chain_add(sampler, llama_sampler_init_temp(Float(options.temperature ?? 0.7)))
123
+ llama_sampler_chain_add(sampler, llama_sampler_init_dist(UInt32.random(in: 0...UInt32.max)))
124
+ }
125
+
126
+ var bytes: [UInt8] = []
127
+ let limit = options.maximumResponseTokens ?? contextLength
128
+ for _ in 0..<limit {
129
+ if cancelled.isCancelled || cached.count >= contextLength - 1 { break }
130
+ let token = llama_sampler_sample(sampler, context, -1)
131
+ if llama_vocab_is_eog(vocab, token) { break }
132
+ bytes += piece(token)
133
+ // A character split across tokens waits for its last byte.
134
+ if let text = String(bytes: bytes, encoding: .utf8) { onText(Self.trimmed(text)) }
135
+ try decode([token])
136
+ cached.append(token)
137
+ }
138
+ return Self.trimmed(String(decoding: bytes, as: UTF8.self))
139
+ }
140
+
141
+ /// The conversation in the model's chat template, ending where the assistant's reply starts.
142
+ func format(_ messages: [Message]) throws -> String {
143
+ if let text = format(messages, template: template) ?? format(messages, template: "chatml") { return text }
144
+ throw LanguageModelError(.unavailable, "the model's chat template is not supported")
145
+ }
146
+
147
+ private func format(_ messages: [Message], template: String) -> String? {
148
+ let roles = messages.map { strdup($0.role) }
149
+ let contents = messages.map { strdup($0.content) }
150
+ defer {
151
+ roles.forEach { free($0) }
152
+ contents.forEach { free($0) }
153
+ }
154
+ var chat = messages.indices.map { llama_chat_message(role: UnsafePointer(roles[$0]), content: UnsafePointer(contents[$0])) }
155
+ let count = chat.count
156
+ var capacity = max(1024, messages.reduce(0) { $0 + $1.content.utf8.count } * 2 + 512)
157
+ var buffer = [CChar](repeating: 0, count: capacity)
158
+ var length = llama_chat_apply_template(template, &chat, count, true, &buffer, Int32(capacity))
159
+ if length > capacity {
160
+ capacity = Int(length) + 1
161
+ buffer = [CChar](repeating: 0, count: capacity)
162
+ length = llama_chat_apply_template(template, &chat, count, true, &buffer, Int32(capacity))
163
+ }
164
+ guard length >= 0 else { return nil }
165
+ return String(decoding: buffer.prefix(Int(length)).map { UInt8(bitPattern: $0) }, as: UTF8.self)
166
+ }
167
+
168
+ private func tokenize(_ text: String) -> [llama_token] {
169
+ let length = Int32(text.utf8.count)
170
+ var capacity = length + 8
171
+ var tokens = [llama_token](repeating: 0, count: Int(capacity))
172
+ var count = llama_tokenize(vocab, text, length, &tokens, capacity, true, true)
173
+ if count < 0 {
174
+ capacity = -count
175
+ tokens = [llama_token](repeating: 0, count: Int(capacity))
176
+ count = llama_tokenize(vocab, text, length, &tokens, capacity, true, true)
177
+ }
178
+ return Array(tokens.prefix(Int(max(count, 0))))
179
+ }
180
+
181
+ private func piece(_ token: llama_token) -> [UInt8] {
182
+ var capacity: Int32 = 64
183
+ var buffer = [CChar](repeating: 0, count: Int(capacity))
184
+ var count = llama_token_to_piece(vocab, token, &buffer, capacity, 0, false)
185
+ if count < 0 {
186
+ capacity = -count
187
+ buffer = [CChar](repeating: 0, count: Int(capacity))
188
+ count = llama_token_to_piece(vocab, token, &buffer, capacity, 0, false)
189
+ }
190
+ return buffer.prefix(Int(max(count, 0))).map { UInt8(bitPattern: $0) }
191
+ }
192
+
193
+ /// Reads `tokens` into the context; a failure leaves it empty, so the next prompt starts over.
194
+ private func decode(_ tokens: [llama_token]) throws {
195
+ var tokens = tokens
196
+ let result = tokens.withUnsafeMutableBufferPointer { buffer in
197
+ llama_decode(context, llama_batch_get_one(buffer.baseAddress, Int32(buffer.count)))
198
+ }
199
+ guard result == 0 else {
200
+ llama_memory_clear(llama_get_memory(context), true)
201
+ cached = []
202
+ throw LanguageModelError(result == 1 ? .context : .unavailable, "the model could not read the conversation (\(result))")
203
+ }
204
+ }
205
+
206
+ private static func trimmed(_ text: String) -> String {
207
+ String(text.drop(while: \.isWhitespace))
208
+ }
209
+ }
210
+
211
+ /// Set once a response should stop; read from the engine's queue.
212
+ final class CancelFlag: @unchecked Sendable {
213
+ private let lock = NSLock()
214
+ private var cancelled = false
215
+
216
+ var isCancelled: Bool {
217
+ lock.withLock { cancelled }
218
+ }
219
+
220
+ func cancel() {
221
+ lock.withLock { cancelled = true }
222
+ }
223
+ }