@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,291 @@
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
+ /// A protocol that defines what types can be used as parameters in a LiteRT-LM tool.
18
+ public protocol ToolParameterValue: Codable {
19
+ /// Returns the JSON schema representation for this type.
20
+ static func getJsonSchema() -> [String: Any]
21
+ }
22
+
23
+ extension String: ToolParameterValue {
24
+ public static func getJsonSchema() -> [String: Any] { ["type": "string"] }
25
+ }
26
+ extension Int: ToolParameterValue {
27
+ public static func getJsonSchema() -> [String: Any] { ["type": "integer"] }
28
+ }
29
+ extension Bool: ToolParameterValue {
30
+ public static func getJsonSchema() -> [String: Any] { ["type": "boolean"] }
31
+ }
32
+ extension Double: ToolParameterValue {
33
+ public static func getJsonSchema() -> [String: Any] { ["type": "number"] }
34
+ }
35
+ extension Float: ToolParameterValue {
36
+ public static func getJsonSchema() -> [String: Any] { ["type": "number"] }
37
+ }
38
+ extension Optional: ToolParameterValue where Wrapped: ToolParameterValue {
39
+ public static func getJsonSchema() -> [String: Any] {
40
+ var schema = Wrapped.getJsonSchema()
41
+ schema["nullable"] = true
42
+ return schema
43
+ }
44
+ }
45
+ extension Array: ToolParameterValue where Element: ToolParameterValue {
46
+ public static func getJsonSchema() -> [String: Any] {
47
+ ["type": "array", "items": Element.getJsonSchema()]
48
+ }
49
+ }
50
+
51
+ /// A non-generic protocol allowing `ToolManager` to inspect `ToolParam` properties at runtime.
52
+ protocol ToolParamProtocol {
53
+ var description: String { get }
54
+ var isRequired: Bool { get }
55
+
56
+ /// Type-erased access to the underlying storage (default value), used for reflection.
57
+ /// This allows `ToolManager` to read default values without knowing the specific generic `Value` type.
58
+ var wrappedValueAny: Any? { get }
59
+
60
+ func getFullSchema() -> [String: Any]
61
+ }
62
+
63
+ /// A property wrapper that defines a parameter for a LiteRT-LM tool.
64
+ ///
65
+ /// Use this property wrapper to mark properties of a `Tool` as parameters that can be passed to
66
+ /// the tool. The `description` will be used to generate the tool's schema.
67
+ ///
68
+ /// Example:
69
+ /// ```
70
+ /// // Example of required parameters:
71
+ /// @ToolParam(description: "The title of the event.")
72
+ /// var title: String
73
+ /// @ToolParam(description: "The date and time of the event in format YYYY-MM-DDTHH:MM:SS.")
74
+ /// var dateTime: String
75
+ ///
76
+ /// // Example of a parameter with a default value:
77
+ /// @ToolParam(description: "The duration of the event in minutes. Default is 30.")
78
+ /// var durationMinutes: Int = 30
79
+ ///
80
+ /// // Example of an optional parameter:
81
+ /// @ToolParam(description: "The topic of the event.")
82
+ /// var topic: String?
83
+ /// ```
84
+ @propertyWrapper
85
+ public struct ToolParam<Value: ToolParameterValue>: Decodable, ToolParamProtocol {
86
+
87
+ /// Holds the actual value of the parameter.
88
+ private var storage: Value?
89
+
90
+ /// A description of the parameter. This is used to to describe the parameter in the schema.
91
+ public let description: String
92
+
93
+ /// Whether the parameter has a default value provided by the developer when declaring the tool.
94
+ private let hasDefaultValue: Bool
95
+
96
+ /// Initializes a parameter with an optional default value.
97
+ public init(wrappedValue: Value? = nil, description: String) {
98
+ self.storage = wrappedValue
99
+ self.description = description
100
+ self.hasDefaultValue = wrappedValue != nil
101
+ }
102
+
103
+ /// The value of the parameter.
104
+ ///
105
+ /// Returns the stored value if available (from a default or decoded input).
106
+ /// Returns `nil` if no value is present and the type `Value` is Optional.
107
+ public var wrappedValue: Value {
108
+ get {
109
+ // If we have data, return it.
110
+ if let value = storage {
111
+ return value
112
+ }
113
+
114
+ // If storage is empty, check if it's an Optional.
115
+ if let nilValue = Optional<Any>.none as? Value {
116
+ return nilValue
117
+ }
118
+
119
+ // It's not optional and we have no data. Throw an error.
120
+ fatalError("ToolParam of type \(Value.self) was not set and has no default value.")
121
+ }
122
+ set { storage = newValue }
123
+ }
124
+
125
+ /// Provides type-erased access to the underlying storage of the parameter.
126
+ ///
127
+ /// Unlike `wrappedValue`, this property returns `nil` if no value is set, instead of crashing.
128
+ /// This is essential for `ToolManager` to safely inspect default values via reflection (Mirror)
129
+ /// before the decoding process occurs.
130
+ var wrappedValueAny: Any? {
131
+ return storage
132
+ }
133
+
134
+ func getFullSchema() -> [String: Any] {
135
+ var schema = Value.getJsonSchema()
136
+ if !description.isEmpty {
137
+ schema["description"] = description
138
+ }
139
+ return schema
140
+ }
141
+
142
+ var isRequired: Bool {
143
+ // If `nil` can be cast to `Value`, then `Value` must be an Optional type.
144
+ let isOptional = Optional<Any>.none as? Value != nil
145
+ return !isOptional && !hasDefaultValue
146
+ }
147
+
148
+ /// Decodes the parameter value from the decoder.
149
+ ///
150
+ /// This initializer is required to conform to the `Decodable` protocol. Since the `Tool` protocol
151
+ /// inherits from `Decodable`, any property wrapper used within a `Tool` must also be `Decodable`.
152
+ ///
153
+ /// When `ToolManager` executes a tool, it uses `JSONDecoder` to decode the JSON arguments
154
+ /// provided by the model into a `Tool` instance. This initializer is called automatically during
155
+ /// that process to populate the `ToolParam`'s internal storage with the value provided by the
156
+ /// model.
157
+ public init(from decoder: Decoder) throws {
158
+ let container = try decoder.singleValueContainer()
159
+
160
+ if container.decodeNil() {
161
+ if let nilValue = Optional<Any>.none as? Value {
162
+ self.storage = nilValue
163
+ } else {
164
+ throw DecodingError.valueNotFound(
165
+ Value.self,
166
+ .init(
167
+ codingPath: decoder.codingPath,
168
+ debugDescription: "Received null for non-optional parameter"))
169
+ }
170
+ } else {
171
+ self.storage = try container.decode(Value.self)
172
+ }
173
+
174
+ self.description = ""
175
+ self.hasDefaultValue = true
176
+ }
177
+ }
178
+
179
+ /// Example of how to define tools:
180
+ /// - Adopt the `Tool` protocol to define a struct/class as a tool.
181
+ /// - Use `@ToolParam` to define the parameters of the tool.
182
+ /// - The allowed parameter types are: String, Int, Bool, Float, Double, and Array of them.
183
+ /// - The return type of `run()` is `Any` and will be converted to JSON/String back to the model.
184
+ /// - Use the Swift Optional type (e.g., `String?`) to indicate that a parameter is optional.
185
+ ///
186
+ /// ```swift
187
+ /// struct GetCurrentWeatherTool: Tool {
188
+ /// static let name = "get_current_weather"
189
+ /// static let description = "Get the current weather"
190
+ ///
191
+ /// @ToolParam(description: "The city and state, e.g. San Francisco, CA")
192
+ /// var location: String
193
+ ///
194
+ /// @ToolParam(description: "The temperature unit to use")
195
+ /// var unit: String = "celsius"
196
+ ///
197
+ /// func run() async throws -> Any {
198
+ /// // In a real application, you would call a weather API here. This is just an example.
199
+ /// let temperature = getTemperature(for: location, in: unit)
200
+ ///
201
+ /// return [
202
+ /// "temperature": temperature,
203
+ /// "unit": unit,
204
+ /// ]
205
+ /// }
206
+ /// }
207
+ /// ```
208
+ /// A protocol a struct/class must conform to be used as a tool by the LiteRT-LM model.
209
+ ///
210
+ /// - SeeAlso: `ToolParam` for defining parameters of the tool.
211
+ public protocol Tool: Decodable {
212
+ /// The unique name of the tool.
213
+ static var name: String { get }
214
+ /// A description of what the tool does.
215
+ static var description: String { get }
216
+
217
+ /// A required zero-argument initializer.
218
+ init()
219
+ /// The logic to execute when the tool is called.
220
+ func run() async throws -> Any
221
+ }
222
+
223
+ extension Tool {
224
+ /// Generates an OpenAPI-compliant JSON schema for the tool.
225
+ public func getSchema() -> [String: Any] {
226
+ var properties: [String: Any] = [:]
227
+ var requiredFields: [String] = []
228
+
229
+ let mirror = Mirror(reflecting: self)
230
+ let useSnakeCase = ExperimentalFlags.convertCamelToSnakeCaseInToolDescription
231
+
232
+ for child in mirror.children {
233
+ guard let label = child.label else { continue }
234
+
235
+ // Clean the name (remove the _ prefix and handle snake_case)
236
+ let cleanName = label.cleanPropertyLabel(useSnakeCase: useSnakeCase)
237
+
238
+ // Check if this property is a ToolParam
239
+ if let param = child.value as? ToolParamProtocol {
240
+ properties[cleanName] = param.getFullSchema()
241
+ if param.isRequired {
242
+ requiredFields.append(cleanName)
243
+ }
244
+ }
245
+ }
246
+
247
+ let toolName = useSnakeCase ? Self.name.camelToSnakeCase() : Self.name
248
+
249
+ var parameters: [String: Any] = [
250
+ "type": "object",
251
+ "properties": properties,
252
+ ]
253
+
254
+ if !requiredFields.isEmpty {
255
+ parameters["required"] = requiredFields
256
+ }
257
+
258
+ var schema: [String: Any] = [
259
+ "type": "function",
260
+ "function": [
261
+ "name": toolName,
262
+ "description": Self.description,
263
+ ],
264
+ ]
265
+
266
+ if !properties.isEmpty {
267
+ var functionBody = schema["function"] as! [String: Any]
268
+ functionBody["parameters"] = parameters
269
+ schema["function"] = functionBody
270
+ }
271
+
272
+ return schema
273
+ }
274
+ }
275
+
276
+ extension String {
277
+ /// Converts a camelCase string to snake_case.
278
+ func camelToSnakeCase() -> String {
279
+ let regex = try! NSRegularExpression(pattern: "([a-z0-9])([A-Z])", options: [])
280
+ let range = NSRange(location: 0, length: self.utf16.count)
281
+ return regex.stringByReplacingMatches(
282
+ in: self, options: [], range: range, withTemplate: "$1_$2"
283
+ ).lowercased()
284
+ }
285
+
286
+ /// Cleans a property label (removes '_' prefix) and optionally converts to snake_case.
287
+ func cleanPropertyLabel(useSnakeCase: Bool) -> String {
288
+ let cleaned = self.hasPrefix("_") ? String(self.dropFirst()) : self
289
+ return useSnakeCase ? cleaned.camelToSnakeCase() : cleaned
290
+ }
291
+ }
@@ -0,0 +1,152 @@
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
+ /// Manages the discovery, schema generation, and execution of tools.
18
+ public class ToolManager {
19
+
20
+ /// A map of tool names to their corresponding Swift Types.
21
+ /// We store the `Type` so we can instantiate new copies via Codable.
22
+ private let toolRegistry: [String: Tool.Type]
23
+
24
+ /// The JSON schema string representing all registered tools.
25
+ /// This is computed once during initialization.
26
+ public let toolsJsonDescription: String
27
+
28
+ /// Initializes the ToolManager with a list of tool instances.
29
+ ///
30
+ /// - Parameter tools: A list of instantiated tools (e.g. `[GetWeatherTool()]`).
31
+ /// These instances are used *only* for schema generation. Execution uses new instances.
32
+ public init(tools: [Tool]) {
33
+ var registry: [String: Tool.Type] = [:]
34
+ var schemaList: [[String: Any]] = []
35
+ let useSnakeCase = ExperimentalFlags.convertCamelToSnakeCaseInToolDescription
36
+
37
+ for tool in tools {
38
+ let name = type(of: tool).name
39
+ let toolNameInModel = useSnakeCase ? name.camelToSnakeCase() : name
40
+ registry[toolNameInModel] = type(of: tool)
41
+
42
+ // Use the provided instance to generate the schema via Mirror
43
+ schemaList.append(tool.getSchema())
44
+ }
45
+
46
+ self.toolRegistry = registry
47
+
48
+ // Serialize the schema list to a JSON string
49
+ if let data = try? JSONSerialization.data(withJSONObject: schemaList, options: []),
50
+ let jsonString = String(data: data, encoding: .utf8)
51
+ {
52
+ self.toolsJsonDescription = jsonString
53
+ } else {
54
+ self.toolsJsonDescription = "[]"
55
+ }
56
+ }
57
+
58
+ /// Executes a tool based on the name and JSON arguments provided by the model.
59
+ ///
60
+ /// - Parameters:
61
+ /// - name: The name of the tool to execute.
62
+ /// - arguments: The arguments as a dictionary.
63
+ /// - Returns: The result of the tool execution. This will be a JSON-compatible type (String, Number, Bool, Array, Dictionary).
64
+ public func execute(name: String, arguments: [String: Any]) async throws -> Any {
65
+ guard let ToolType = toolRegistry[name] else {
66
+ throw LiteRTLMError.tool(.notFound(name: name))
67
+ }
68
+
69
+ // 1. Create a template instance to access default values.
70
+ // This allows us to handle optional parameters that the model might omit from its output.
71
+ // Swift Codable requires all keys to be present.
72
+ // By manually merging defaults, we ensure decoding succeeds.
73
+ let defaultTool = ToolType.init()
74
+ var defaults: [String: Any] = [:]
75
+
76
+ // 2. Reflect to extract default values.
77
+ let mirror = Mirror(reflecting: defaultTool)
78
+ let useSnakeCase = ExperimentalFlags.convertCamelToSnakeCaseInToolDescription
79
+
80
+ for child in mirror.children {
81
+ guard let label = child.label else { continue }
82
+
83
+ // Clean the property wrapper name (remove _ prefix and handle snake_case)
84
+ let paramName = label.cleanPropertyLabel(useSnakeCase: useSnakeCase)
85
+
86
+ if let toolParam = child.value as? ToolParamProtocol {
87
+ if let defaultValue = toolParam.wrappedValueAny {
88
+ defaults[paramName] = defaultValue
89
+ } else if !toolParam.isRequired {
90
+ // If it's not required (i.e. optional or has default),
91
+ // but currently nil, we should still provide a null value
92
+ // so the decoder doesn't fail on missing key.
93
+ defaults[paramName] = NSNull()
94
+ }
95
+ }
96
+ }
97
+
98
+ // 3. Merge provided arguments over the defaults.
99
+ // The `arguments` take precedence.
100
+ let mergedArgs = defaults.merging(arguments) { (_, new) in new }
101
+
102
+ let argsData = try JSONSerialization.data(withJSONObject: mergedArgs)
103
+
104
+ let decoder = JSONDecoder()
105
+ if useSnakeCase {
106
+ decoder.keyDecodingStrategy = .convertFromSnakeCase
107
+ }
108
+
109
+ // Create a new tool instance from the JSON arguments.
110
+ // This populates the @ToolParam properties automatically.
111
+ let toolInstance = try decoder.decode(ToolType, from: argsData)
112
+
113
+ // Run the tool
114
+ let result = try await toolInstance.run()
115
+
116
+ // Normalize the result to ensure it is strictly JSON-compatible (mimicking Android logic)
117
+ return normalizeResult(result)
118
+ }
119
+
120
+ /// Helper to recursively convert any result into a JSON-Serialization friendly format.
121
+ /// - Collections: Recursively normalized.
122
+ /// - Primitives: Passed through.
123
+ /// - Custom Objects: Converted to String.
124
+ private func normalizeResult(_ value: Any) -> Any {
125
+ // Handle Dictionaries
126
+ if let dict = value as? [String: Any] {
127
+ var normalizedDict: [String: Any] = [:]
128
+ for (key, val) in dict {
129
+ normalizedDict[key] = normalizeResult(val)
130
+ }
131
+ return normalizedDict
132
+ }
133
+
134
+ // Handle Arrays
135
+ if let array = value as? [Any] {
136
+ return array.map { normalizeResult($0) }
137
+ }
138
+
139
+ // Handle Primitives (String, Number, Bool)
140
+ if value is String || value is Int || value is Double || value is Float || value is Bool {
141
+ return value
142
+ }
143
+
144
+ // Handle Void
145
+ if value is Void {
146
+ return ""
147
+ }
148
+
149
+ // Fallback for Custom Objects
150
+ return String(describing: value)
151
+ }
152
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capgo/capacitor-llm",
3
- "version": "8.0.19",
3
+ "version": "8.1.0",
4
4
  "description": "Adds support for LLM locally runned for Capacitor",
5
5
  "main": "dist/plugin.cjs.js",
6
6
  "module": "dist/esm/index.js",
@@ -32,9 +32,12 @@
32
32
  "llm",
33
33
  "local",
34
34
  "gemma",
35
+ "gemma4",
35
36
  "gemma3",
36
37
  "gemma3-270m",
37
38
  "gemma2-2b",
39
+ "litert",
40
+ "litert-lm",
38
41
  "apple-intelligence",
39
42
  "gpt"
40
43
  ],
@@ -53,9 +56,7 @@
53
56
  "clean": "rimraf ./dist",
54
57
  "watch": "tsc --watch",
55
58
  "prepublishOnly": "bun run build",
56
- "check:wiring": "node scripts/check-capacitor-plugin-wiring.mjs",
57
- "example:install": "cd example-app && bun install --frozen-lockfile",
58
- "example:build": "bun run build && cd example-app && bun install --frozen-lockfile && bun run build"
59
+ "check:wiring": "node scripts/check-capacitor-plugin-wiring.mjs"
59
60
  },
60
61
  "devDependencies": {
61
62
  "@capacitor/android": "^8.0.0",
@@ -80,7 +81,7 @@
80
81
  },
81
82
  "peerDependencies": {
82
83
  "@capacitor/core": ">=8.0.0",
83
- "@mediapipe/tasks-genai": "^0.10.27"
84
+ "@mediapipe/tasks-genai": "^0.10.24"
84
85
  },
85
86
  "prettier": "@ionic/prettier-config",
86
87
  "swiftlint": "@ionic/swiftlint-config",