@capgo/capacitor-llm 8.0.19 → 8.1.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.
@@ -0,0 +1,142 @@
1
+ // Copyright 2026 Google LLC
2
+ //
3
+ // Licensed under the Apache License, Version 2.0 (the "License");
4
+ // you may not use this file except in compliance with the License.
5
+ // You may obtain a copy of the License at
6
+ //
7
+ // https://www.apache.org/licenses/LICENSE-2.0
8
+ //
9
+ // Unless required by applicable law or agreed to in writing, software
10
+ // distributed under the License is distributed on an "AS IS" BASIS,
11
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ // See the License for the specific language governing permissions and
13
+ // limitations under the License.
14
+ import Foundation
15
+ import OSLog
16
+
17
+ /// A struct to manage flags for APIs that are not yet considered mature.
18
+ ///
19
+ /// These flags guard experimental APIs that may still undergo significant changes.
20
+ /// To use any experimental flags, first call `ExperimentalFlags.optIntoExperimentalAPIs()`.
21
+ public struct ExperimentalFlags {
22
+ private static var optedIn = false
23
+
24
+ private static let logger = Logger(
25
+ subsystem: "com.google.odml.litertlm.swift",
26
+ category: "ExperimentalFlags"
27
+ )
28
+
29
+ public static func optIntoExperimentalAPIs() {
30
+ guard !optedIn else { return }
31
+ logger.info("EXPERIMENTAL: LiteRTLM: Opting into experimental APIs....")
32
+ optedIn = true
33
+ }
34
+
35
+ private static var _enableBenchmark: Bool = false
36
+
37
+ public static var enableBenchmark: Bool {
38
+ get { return _enableBenchmark }
39
+ set {
40
+ guard optedIn else {
41
+ logger.error("LiteRTLM: Must opt into experimental APIs before setting this flag.")
42
+ return
43
+ }
44
+ _enableBenchmark = newValue
45
+ }
46
+ }
47
+
48
+ private static var _convertCamelToSnakeCaseInToolDescription: Bool = true
49
+
50
+ /// Whether to convert the function and parameter names in to snake case for
51
+ /// tool calling.
52
+ ///
53
+ /// Swift idiomatic style uses camelCase for names. However, many large
54
+ /// language models are predominantly trained on datasets where snake_case is
55
+ /// common.
56
+ ///
57
+ /// By default, this API converts Swift camelCase names to snake_case to
58
+ /// potentially improve tool calling performance with models more familiar
59
+ /// with snake_case.
60
+ ///
61
+ /// Set this flag to `false` if your model is specifically trained with
62
+ /// camelCase tool descriptions to skip the conversion. Otherwise, the default
63
+ /// of `true` (using snake_case) is recommended.
64
+ ///
65
+ /// Note: This flag is read only when a new [Conversation] is created.
66
+ /// Changing this value will not affect any existing [Conversation] instances.
67
+ public static var convertCamelToSnakeCaseInToolDescription: Bool {
68
+ get { return _convertCamelToSnakeCaseInToolDescription }
69
+ set {
70
+ guard optedIn else {
71
+ logger.error("LiteRTLM: Must opt into experimental APIs before setting this flag.")
72
+ return
73
+ }
74
+ _convertCamelToSnakeCaseInToolDescription = newValue
75
+ }
76
+ }
77
+
78
+ private static var _enableConversationConstrainedDecoding: Bool = false
79
+
80
+ /// Whether to enable conversation constrained decoding. This is primarily
81
+ /// used for function calling.
82
+ ///
83
+ /// Note: This flag is read only when a new [Conversation] is created.
84
+ /// Changing this value will not affect any existing [Conversation] instances.
85
+ public static var enableConversationConstrainedDecoding: Bool {
86
+ get { return _enableConversationConstrainedDecoding }
87
+ set {
88
+ guard optedIn else {
89
+ logger.error("LiteRTLM: Must opt into experimental APIs before setting this flag.")
90
+ return
91
+ }
92
+ _enableConversationConstrainedDecoding = newValue
93
+ }
94
+ }
95
+
96
+ private static var _enableSpeculativeDecoding: Bool? = nil
97
+
98
+ /// Whether to enable speculative decoding.
99
+ ///
100
+ /// If true, enable speculative decoding; an error will be thrown if the model does not support it.
101
+ /// If false, disable speculative decoding.
102
+ ///
103
+ /// Note: This flag is read only when a new [Engine] is created. Changing this value will not
104
+ /// affect any existing [Engine] or [Conversation] instances.
105
+ public static var enableSpeculativeDecoding: Bool? {
106
+ get { return _enableSpeculativeDecoding }
107
+ set {
108
+ guard optedIn else {
109
+ logger.error("LiteRTLM: Must opt into experimental APIs before setting this flag.")
110
+ return
111
+ }
112
+ _enableSpeculativeDecoding = newValue
113
+ }
114
+ }
115
+
116
+ private static var _visualTokenBudget: Int32? = nil
117
+
118
+ /// The visual token budget.
119
+ ///
120
+ /// The number of visual tokens that the model can generate for a single image. If null, there is
121
+ /// no budget limit and the engine use as much as needed.
122
+ ///
123
+ /// Currently, this is only supported by Gemma4. If this flag is set for a non-Gemma4 model, it
124
+ /// will result in a no-ops. The Gemma4 budget options are 70, 140, 280, 560, or 1120 tokens. See
125
+ /// https://ai.google.dev/gemma/docs/capabilities/vision#variable-resolution for more details.
126
+ ///
127
+ /// Note: This flag takes effect immediately and change alter the behaivor of created
128
+ /// [Conversation].
129
+ public static var visualTokenBudget: Int32? {
130
+ get { return _visualTokenBudget }
131
+ set {
132
+ guard optedIn else {
133
+ logger.error("LiteRTLM: Must opt into experimental APIs before setting this flag.")
134
+ return
135
+ }
136
+ _visualTokenBudget = newValue
137
+ }
138
+ }
139
+
140
+ // Prevent initializing the struct
141
+ private init() {}
142
+ }
@@ -0,0 +1,156 @@
1
+ // Copyright 2026 Google LLC
2
+ //
3
+ // Licensed under the Apache License, Version 2.0 (the "License");
4
+ // you may not use this file except in compliance with the License.
5
+ // You may obtain a copy of the License at
6
+ //
7
+ // https://www.apache.org/licenses/LICENSE-2.0
8
+ //
9
+ // Unless required by applicable law or agreed to in writing, software
10
+ // distributed under the License is distributed on an "AS IS" BASIS,
11
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ // See the License for the specific language governing permissions and
13
+ // limitations under the License.
14
+
15
+ import Foundation
16
+
17
+ /// Errors thrown by the LiteRT-LM Swift API.
18
+ public enum LiteRTLMError: Error, LocalizedError, Equatable {
19
+ case engine(EngineError)
20
+ case conversation(ConversationError)
21
+ case config(ConfigError)
22
+ case tool(ToolError)
23
+ case message(MessageError)
24
+
25
+ public var errorDescription: String? {
26
+ switch self {
27
+ case .engine(let error): return error.errorDescription
28
+ case .conversation(let error): return error.errorDescription
29
+ case .config(let error): return error.errorDescription
30
+ case .tool(let error): return error.errorDescription
31
+ case .message(let error): return error.errorDescription
32
+ }
33
+ }
34
+
35
+ /// Specific errors related to the `Engine`.
36
+ public enum EngineError: Error, LocalizedError, Equatable {
37
+ case alreadyInitialized
38
+ case failedToCreateSettings
39
+ case failedToCreateEngine
40
+ case notInitialized
41
+ case failedToCreateSessionConfig
42
+ case failedToCreateConversationConfig
43
+ case failedToCreateConversation
44
+
45
+ public var errorDescription: String? {
46
+ switch self {
47
+ case .alreadyInitialized:
48
+ return "Engine is already initialized."
49
+ case .failedToCreateSettings:
50
+ return "Failed to create engine settings."
51
+ case .failedToCreateEngine:
52
+ return "Failed to create engine."
53
+ case .notInitialized:
54
+ return "Engine is not initialized."
55
+ case .failedToCreateSessionConfig:
56
+ return "Failed to create session config."
57
+ case .failedToCreateConversationConfig:
58
+ return "Failed to create conversation config."
59
+ case .failedToCreateConversation:
60
+ return "Failed to create conversation."
61
+ }
62
+ }
63
+ }
64
+
65
+ /// Specific errors related to `Conversation`.
66
+ public enum ConversationError: Error, LocalizedError, Equatable {
67
+ case notAlive
68
+ case failedToSerializeMessage
69
+ case invalidResponse(String)
70
+ case recurringToolCallLimitExceeded(limit: Int)
71
+ case failedToStartStream(status: Int)
72
+ case invalidJson(String)
73
+ case toolExecutionError(name: String, error: String)
74
+ case benchmarkNotEnabled
75
+ case benchmarkInfoUnavailable
76
+
77
+ public var errorDescription: String? {
78
+ switch self {
79
+ case .notAlive:
80
+ return "Conversation is not alive."
81
+ case .failedToSerializeMessage:
82
+ return "Failed to serialize message to JSON string."
83
+ case .invalidResponse(let details):
84
+ return "Invalid response from native layer: \(details)"
85
+ case .recurringToolCallLimitExceeded(let limit):
86
+ return "Exceeded recurring tool call limit of \(limit)"
87
+ case .failedToStartStream(let status):
88
+ return "Failed to start stream. Status: \(status)"
89
+ case .invalidJson(let details):
90
+ return "Invalid JSON: \(details)"
91
+ case .toolExecutionError(let name, let error):
92
+ return "Error processing tool call \(name): \(error)"
93
+ case .benchmarkNotEnabled:
94
+ return """
95
+ Benchmark flag is not enabled. Please enable the flag by setting setting \
96
+ ExperimentalFlags.enableBenchmark to true before initializing the Engine.
97
+ """
98
+ case .benchmarkInfoUnavailable:
99
+ return "Failed to get benchmark info."
100
+ }
101
+ }
102
+ }
103
+
104
+ public enum ConfigError: Error, LocalizedError, Equatable {
105
+ case invalidMaxNumTokens
106
+ case invalidMaxNumImages(count: Int)
107
+ case invalidTopK
108
+ case invalidTopP
109
+ case invalidTemperature
110
+ case multipleSystemMessages
111
+
112
+ public var errorDescription: String? {
113
+ switch self {
114
+ case .invalidMaxNumTokens:
115
+ return "maxNumTokens must be positive or nil (use the default from model or engine)."
116
+ case .invalidMaxNumImages:
117
+ return "maxNumImages must be non-negative or nil (use the default from model or engine)."
118
+ case .invalidTopK:
119
+ return "topK should be positive."
120
+ case .invalidTopP:
121
+ return "topP not between 0 and 1"
122
+ case .invalidTemperature:
123
+ return "temperature should be non-negative"
124
+ case .multipleSystemMessages:
125
+ return "Cannot set both systemMessage and have system messages in initialMessages."
126
+ }
127
+ }
128
+ }
129
+
130
+ /// Specific errors related to tools.
131
+ public enum ToolError: Error, LocalizedError, Equatable {
132
+ case notFound(name: String)
133
+
134
+ public var errorDescription: String? {
135
+ switch self {
136
+ case .notFound(let name):
137
+ return "Tool '\(name)' not found."
138
+ }
139
+ }
140
+ }
141
+
142
+ /// Specific errors related to messages.
143
+ public enum MessageError: Error, LocalizedError, Equatable {
144
+ case failedToConvertToJson
145
+ case invalidContent
146
+
147
+ public var errorDescription: String? {
148
+ switch self {
149
+ case .failedToConvertToJson:
150
+ return "Failed to convert Message to JSON string."
151
+ case .invalidContent:
152
+ return "No content found in JSON string. Cannot create Message."
153
+ }
154
+ }
155
+ }
156
+ }
@@ -0,0 +1,225 @@
1
+ // Copyright 2026 Google LLC
2
+ //
3
+ // Licensed under the Apache License, Version 2.0 (the "License");
4
+ // you may not use this file except in compliance with the License.
5
+ // You may obtain a copy of the License at
6
+ //
7
+ // https://www.apache.org/licenses/LICENSE-2.0
8
+ //
9
+ // Unless required by applicable law or agreed to in writing, software
10
+ // distributed under the License is distributed on an "AS IS" BASIS,
11
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ // See the License for the specific language governing permissions and
13
+ // limitations under the License.
14
+
15
+ import Foundation
16
+ import OSLog
17
+
18
+ /// The role of the message in a conversation.
19
+ public enum Role: String {
20
+ case system
21
+ case user
22
+ case model
23
+ case tool
24
+ }
25
+
26
+ /// Represents a content in the `Message` of the conversation.
27
+ ///
28
+ /// Example Usage:
29
+ /// ```swift
30
+ /// let textContent = Content.text("What's the capital of France?")
31
+ /// let imageContent = Content.imageData(imageData)
32
+ /// let audioContent = Content.audioFile(path: "/path/to/audio.wav")
33
+ /// ```
34
+ public enum Content {
35
+ /// Text.
36
+ case text(String)
37
+ /// Image provided as raw bytes.
38
+ case imageData(Data)
39
+ /// Image provided by an absolute file path.
40
+ case imageFile(String)
41
+ /// Audio provided as raw bytes.
42
+ case audioData(Data)
43
+ /// Audio provided by an absolute file path.
44
+ case audioFile(String)
45
+
46
+ /// Convert to JSON format. Used internally.
47
+ var toJson: [String: String] {
48
+ switch self {
49
+ case .text(let text):
50
+ return ["type": "text", "text": text]
51
+ case .imageData(let bytes):
52
+ return ["type": "image", "blob": bytes.base64EncodedString()]
53
+ case .imageFile(let absPath):
54
+ return ["type": "image", "path": absPath]
55
+ case .audioData(let bytes):
56
+ return ["type": "audio", "blob": bytes.base64EncodedString()]
57
+ case .audioFile(let absPath):
58
+ return ["type": "audio", "path": absPath]
59
+ }
60
+ }
61
+ }
62
+
63
+ /// Represents a message in the conversation. A message can contain multiple `Content`s.
64
+ ///
65
+ /// Example Usage:
66
+ /// ```swift
67
+ /// let textMessage = Message("What's the capital of France?")
68
+ /// let modelMessage = Message("The capital of France is Paris.", role: .model)
69
+ /// let audioContentMessage = Message(of: someAudioContent)
70
+ /// let multiContentMessage = Message(of: someText, someImageContent)
71
+ /// let multiContentMessageFromArray = Message(contents: [someText, someImageContent])
72
+ /// ```
73
+ public struct Message {
74
+
75
+ private let logger = Logger(
76
+ subsystem: "com.google.odml.litertlm.swift",
77
+ category: "Message"
78
+ )
79
+
80
+ /// The role of the message.
81
+ public let role: Role
82
+
83
+ /// The contents of the message.
84
+ public let contents: Contents
85
+
86
+ /// The channels of the message.
87
+ public let channels: [String: String]
88
+
89
+ // TODO: (b/459796564): Update constructor to `Message.role("messageTextHere")`
90
+ /// Creates a `Message` from a text string.
91
+ /// - Parameter text: The text content of the message.
92
+ public init(_ text: String, role: Role = .user, channels: [String: String] = [:]) {
93
+ self.init(contents: Contents(contents: [.text(text)]), role: role, channels: channels)
94
+ }
95
+
96
+ /// Creates a `Message` from one or more `Content`s.
97
+ /// - Parameter contents: The list of contents for this message.
98
+ public init(of contents: Content..., role: Role = .user) {
99
+ precondition(!contents.isEmpty, "Contents should not be empty.")
100
+ self.contents = Contents(contents: contents)
101
+ self.role = role
102
+ self.channels = [:]
103
+ }
104
+
105
+ /// Creates a `Message` from a list of `Content`s.
106
+ /// - Parameter contents: The list of contents for this message.
107
+ public init(contents: [Content], role: Role = .user, channels: [String: String] = [:]) {
108
+ precondition(
109
+ !contents.isEmpty || !channels.isEmpty, "Contents and channels should not both be empty.")
110
+ self.contents = Contents(contents: contents)
111
+ self.role = role
112
+ self.channels = channels
113
+ }
114
+
115
+ /// Creates a `Message` from a `Contents` object.
116
+ /// - Parameter contents: The contents object for this message.
117
+ public init(contents: Contents, role: Role = .user, channels: [String: String] = [:]) {
118
+ precondition(
119
+ !contents.isEmpty || !channels.isEmpty, "Contents and channels should not both be empty.")
120
+ self.contents = contents
121
+ self.role = role
122
+ self.channels = channels
123
+ }
124
+
125
+ /// Convert to JSON format. Used internally.
126
+ var toJson: [String: Any] {
127
+ var dict: [String: Any] = ["role": role.rawValue]
128
+ if !contents.isEmpty {
129
+ dict["content"] = contents.toJson
130
+ }
131
+ if !channels.isEmpty {
132
+ dict["channels"] = channels
133
+ }
134
+ return dict
135
+ }
136
+
137
+ /// Convenience property to get the text content of the message.
138
+ /// Returns a string by concatenating all text content items, separated by a space.
139
+ /// Returns an empty string if no text content exists.
140
+ public var toString: String {
141
+ return contents.toString
142
+ }
143
+
144
+ /// A computed property that returns the JSON string representation
145
+ /// of the Message, or nil if conversion fails.
146
+ var jsonString: String {
147
+ get throws {
148
+ let jsonData = try JSONSerialization.data(withJSONObject: self.toJson, options: [])
149
+ guard let resultString = String(data: jsonData, encoding: .utf8) else {
150
+ throw LiteRTLMError.message(.failedToConvertToJson)
151
+ }
152
+ return resultString
153
+ }
154
+ }
155
+ }
156
+
157
+ /// Represents a collection of `Content` items in the `Message` of the conversation.
158
+ ///
159
+ /// Example Usage:
160
+ /// ```swift
161
+ /// let contents = Contents(contents: [.text("Hello")])
162
+ /// ```
163
+ public struct Contents: RandomAccessCollection {
164
+ public typealias Element = Content
165
+ public typealias Index = Int
166
+
167
+ /// The list of underlying content items.
168
+ public var contents: [Content]
169
+
170
+ public var startIndex: Int { contents.startIndex }
171
+ public var endIndex: Int { contents.endIndex }
172
+
173
+ public subscript(position: Int) -> Content {
174
+ return contents[position]
175
+ }
176
+
177
+ /// Creates a `Contents` from a list of `Content`s.
178
+ public init(contents: [Content]) {
179
+ self.contents = contents
180
+ }
181
+
182
+ /// Creates an empty `Contents`.
183
+ public static func empty() -> Contents {
184
+ return Contents(contents: [])
185
+ }
186
+
187
+ /// Creates a `Contents` from a text string.
188
+ public static func of(_ text: String) -> Contents {
189
+ return Contents(contents: [.text(text)])
190
+ }
191
+
192
+ /// Creates a `Contents` from one or more `Content`s.
193
+ public static func of(_ contents: Content...) -> Contents {
194
+ return Contents(contents: contents)
195
+ }
196
+
197
+ /// Creates a `Contents` from a list of `Content`s.
198
+ public static func of(_ contents: [Content]) -> Contents {
199
+ return Contents(contents: contents)
200
+ }
201
+
202
+ /// Convert to JSON format. Used internally.
203
+ var toJson: [[String: String]] {
204
+ return contents.map { $0.toJson }
205
+ }
206
+
207
+ /// A computed property that returns the JSON string representation
208
+ /// of the Contents array.
209
+ var jsonString: String {
210
+ get throws {
211
+ let jsonData = try JSONSerialization.data(withJSONObject: self.toJson, options: [])
212
+ guard let resultString = String(data: jsonData, encoding: .utf8) else {
213
+ throw LiteRTLMError.message(.failedToConvertToJson)
214
+ }
215
+ return resultString
216
+ }
217
+ }
218
+
219
+ /// Returns a string by concatenating all text content items, separated by a space.
220
+ public var toString: String {
221
+ contents.compactMap { content in
222
+ if case .text(let str) = content { return str } else { return nil }
223
+ }.joined(separator: " ")
224
+ }
225
+ }