@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,472 @@
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
+ import CLiteRTLM
18
+
19
+ typealias CConversationHandle = OpaquePointer
20
+
21
+ private let logger = Logger(
22
+ subsystem: "com.google.odml.litertlm.swift",
23
+ category: "Conversation"
24
+ )
25
+
26
+ private let recurringToolCallLimit = 25
27
+
28
+ /// Represents a conversation with the LiteRT-LM model.
29
+ ///
30
+ /// Example usage:
31
+ /// ```swift
32
+ /// // Assuming 'engine' is an instance of Engine
33
+ /// let conversation = try await engine.createConversation()
34
+ ///
35
+ /// // Send a message and get the response.
36
+ /// let response = try conversation.sendMessage(Message("Hello world"))
37
+ ///
38
+ /// // Send a message async with response chunks as AsyncThrowingStream.
39
+ /// for try await chunk in conversation.sendMessageStream(Message("Hello world")) {
40
+ /// print(chunk.text)
41
+ /// }
42
+ /// ```
43
+ ///
44
+ /// This class facilitates interaction with the LiteRT-LM model by handling message sending
45
+ /// and response reception.
46
+ public class Conversation {
47
+ private let logger = Logger(
48
+ subsystem: "com.google.ai.edge.litertlm.swift",
49
+ category: "Conversation"
50
+ )
51
+
52
+ private var handle: CConversationHandle?
53
+ private let toolManager: ToolManager
54
+
55
+ /// Whether the conversation is alive and ready to be used.
56
+ public var isAlive: Bool {
57
+ return handle != nil
58
+ }
59
+
60
+ init(handle: CConversationHandle, toolManager: ToolManager) {
61
+ self.handle = handle
62
+ self.toolManager = toolManager
63
+ }
64
+
65
+ deinit {
66
+ if let handle = handle {
67
+ litert_lm_conversation_delete(handle)
68
+ }
69
+ }
70
+
71
+ /// Sends a message to the model and returns the response. This is a synchronous call.
72
+ ///
73
+ /// - Parameter message: The message to send to the model.
74
+ /// - Parameter extraContext: The extra context to send to the model.
75
+ /// - Returns: The model's response message.
76
+ /// - Throws: A `LiteRTLMError` if sending the message fails or the model
77
+ /// returns an invalid response.
78
+ public func sendMessage(_ message: Message, extraContext: [String: Any]? = nil) async throws
79
+ -> Message
80
+ {
81
+ let handle = try checkIsAlive()
82
+
83
+ var currentMessageJson: [String: Any] = message.toJson
84
+
85
+ for _ in 0..<recurringToolCallLimit {
86
+ let (responseJson, responseString) = try attemptSendMessage(
87
+ handle: handle, messageJson: currentMessageJson, extraContext: extraContext)
88
+
89
+ guard let toolCalls = responseJson["tool_calls"] as? [[String: Any]] else {
90
+ if responseJson["content"] != nil || responseJson["channels"] != nil {
91
+ return try Conversation.jsonToMessage(responseString)
92
+ } else {
93
+ throw LiteRTLMError.conversation(.invalidResponse(responseString))
94
+ }
95
+ }
96
+ currentMessageJson = try await handleToolCalls(toolCalls)
97
+ }
98
+ throw LiteRTLMError.conversation(.recurringToolCallLimitExceeded(limit: recurringToolCallLimit))
99
+ }
100
+
101
+ private func attemptSendMessage(
102
+ handle: CConversationHandle, messageJson: [String: Any], extraContext: [String: Any]?
103
+ ) throws
104
+ -> (responseJson: [String: Any], responseString: String)
105
+ {
106
+ let messageData = try JSONSerialization.data(withJSONObject: messageJson)
107
+ guard let messageString = String(data: messageData, encoding: .utf8) else {
108
+ throw LiteRTLMError.conversation(.failedToSerializeMessage)
109
+ }
110
+
111
+ var extraContextString: String? = nil
112
+ if let extraContext = extraContext, !extraContext.isEmpty,
113
+ let extraData = try? JSONSerialization.data(withJSONObject: extraContext)
114
+ {
115
+ extraContextString = String(data: extraData, encoding: .utf8)
116
+ }
117
+ let optionalArgs = litert_lm_conversation_optional_args_create()
118
+ if let visualTokenBudget = ExperimentalFlags.visualTokenBudget {
119
+ litert_lm_conversation_optional_args_set_visual_token_budget(optionalArgs, Int32(visualTokenBudget))
120
+ }
121
+ defer { litert_lm_conversation_optional_args_delete(optionalArgs) }
122
+
123
+ guard let responsePtr = litert_lm_conversation_send_message(
124
+ handle, messageString, extraContextString, optionalArgs)
125
+ else {
126
+ throw LiteRTLMError.conversation(.invalidResponse("Native sendMessage returned null."))
127
+
128
+ }
129
+ // Delete the response pointer at the end of each iteration. Handled by defer block.
130
+ let responsePtrRef = responsePtr
131
+ defer { litert_lm_json_response_delete(responsePtrRef) }
132
+
133
+ guard let responseChars = litert_lm_json_response_get_string(responsePtr) else {
134
+ throw LiteRTLMError.conversation(
135
+ .invalidResponse("Native get string for response returned null."))
136
+ }
137
+ let responseString = String(cString: responseChars)
138
+
139
+ guard let responseData = responseString.data(using: .utf8),
140
+ let responseJson = try JSONSerialization.jsonObject(with: responseData) as? [String: Any]
141
+ else {
142
+ throw LiteRTLMError.conversation(.invalidJson("Failed to parse native response JSON."))
143
+ }
144
+ return (responseJson, responseString)
145
+ }
146
+
147
+ fileprivate func handleToolCalls(_ toolCalls: [[String: Any]]) async throws -> [String: Any] {
148
+ var toolResponses: [[String: Any]] = []
149
+
150
+ for toolCall in toolCalls {
151
+ guard let function = toolCall["function"] as? [String: Any],
152
+ let name = function["name"] as? String,
153
+ let argsObject = function["arguments"] as? [String: Any]
154
+ else {
155
+ continue
156
+ }
157
+ do {
158
+ let result = try await toolManager.execute(name: name, arguments: argsObject)
159
+
160
+ toolResponses.append([
161
+ "type": "tool_response",
162
+ "name": name,
163
+ "response": result,
164
+ ])
165
+ } catch {
166
+ throw LiteRTLMError.conversation(.toolExecutionError(name: name, error: "\(error)"))
167
+ }
168
+ }
169
+
170
+ return ["role": "tool", "content": toolResponses]
171
+ }
172
+
173
+ /// Throws an error if the conversation is not alive.
174
+ ///
175
+ /// - Returns: The `OpaquePointer` handle if the conversation is alive.
176
+ /// - Throws: A `LiteRTLMError` if `handle` is nil, indicating the conversation is not alive.
177
+ fileprivate func checkIsAlive() throws -> OpaquePointer {
178
+ guard let handle else {
179
+ throw LiteRTLMError.conversation(.notAlive)
180
+ }
181
+ return handle
182
+ }
183
+
184
+ /// Sends a message to the model and returns an async stream of response chunks.
185
+ ///
186
+ /// - Parameter message: The message to send.
187
+ /// - Parameter extraContext: The extra context to send to the model.
188
+ /// - Returns: An async throwing stream of `Message` chunks.
189
+ public func sendMessageStream(_ message: Message, extraContext: [String: Any]? = nil)
190
+ -> AsyncThrowingStream<Message, Error>
191
+ {
192
+ return AsyncThrowingStream { continuation in
193
+ do {
194
+ let handle = try self.checkIsAlive()
195
+ let messageJson: [String: Any] = message.toJson
196
+ let context = StreamContext(continuation: continuation, conversation: self)
197
+
198
+ try self.sendToStream(
199
+ handle: handle, messageJson: messageJson, extraContext: extraContext, context: context)
200
+ } catch {
201
+ continuation.finish(throwing: error)
202
+ }
203
+ }
204
+ }
205
+
206
+ /// Sends a message to the model and handles the response via a streaming callback.
207
+ ///
208
+ /// This function is used internally by `sendMessageStream` and for handling
209
+ /// subsequent tool call responses within the stream.
210
+ ///
211
+ /// - Parameters:
212
+ /// - handle: The `CConversationHandle` for the current conversation.
213
+ /// - messageJson: The message to send, represented as a JSON dictionary.
214
+ /// - extraContext: The extra context to send to the model.
215
+ /// - context: The `StreamContext` containing the `AsyncThrowingStream.Continuation`
216
+ /// and other state for the streaming process.
217
+ /// - Throws: A `LiteRTLMError` if the message fails to send or the response is invalid.
218
+ /// native `send_message_stream` call fails.
219
+ func sendToStream(
220
+ handle: CConversationHandle,
221
+ messageJson: [String: Any],
222
+ extraContext: [String: Any]? = nil,
223
+ context: StreamContext
224
+ ) throws {
225
+ let messageData = try JSONSerialization.data(withJSONObject: messageJson)
226
+ guard let messageString = String(data: messageData, encoding: .utf8) else {
227
+ throw LiteRTLMError.conversation(.failedToSerializeMessage)
228
+ }
229
+
230
+ var extraContextString: String? = nil
231
+ if let extraContext = extraContext, !extraContext.isEmpty,
232
+ let extraData = try? JSONSerialization.data(withJSONObject: extraContext)
233
+ {
234
+ extraContextString = String(data: extraData, encoding: .utf8)
235
+ }
236
+
237
+ let optionalArgs = litert_lm_conversation_optional_args_create()
238
+ if let visualTokenBudget = ExperimentalFlags.visualTokenBudget {
239
+ litert_lm_conversation_optional_args_set_visual_token_budget(optionalArgs, Int32(visualTokenBudget))
240
+ }
241
+ defer { litert_lm_conversation_optional_args_delete(optionalArgs) }
242
+
243
+ let contextPtr = Unmanaged.passRetained(context).toOpaque()
244
+
245
+ let status = litert_lm_conversation_send_message_stream(
246
+ handle,
247
+ messageString,
248
+ extraContextString,
249
+ optionalArgs,
250
+ streamCallback,
251
+ contextPtr
252
+ )
253
+
254
+ guard status == 0 else {
255
+ Unmanaged<StreamContext>.fromOpaque(contextPtr).release()
256
+ throw LiteRTLMError.conversation(.failedToStartStream(status: Int(status)))
257
+ }
258
+ }
259
+
260
+ /// Cancels the ongoing asynchronous inference process.
261
+ public func cancel() throws {
262
+ let handle = try checkIsAlive()
263
+ litert_lm_conversation_cancel_process(handle)
264
+ }
265
+
266
+ /// Renders the message into a string for testing and logging.
267
+ ///
268
+ /// This function does not need to be called for actual message sending, as the `sendMessage` and
269
+ /// `sendMessageStream` functions will handle rendering internally.
270
+ ///
271
+ /// - Parameter message: The message to render.
272
+ /// - Returns: The rendered message string.
273
+ /// - Throws: A `LiteRTLMError` if the conversation is not alive, serializing fails, or rendering fails.
274
+ public func renderMessageIntoString(_ message: Message) throws -> String {
275
+ let handle = try checkIsAlive()
276
+ let messageData = try JSONSerialization.data(withJSONObject: message.toJson)
277
+ guard let messageString = String(data: messageData, encoding: .utf8) else {
278
+ throw LiteRTLMError.conversation(.failedToSerializeMessage)
279
+ }
280
+
281
+ guard let cString = litert_lm_conversation_render_message_to_string(handle, messageString)
282
+ else {
283
+ throw LiteRTLMError.conversation(.invalidResponse("Failed to render message into string."))
284
+ }
285
+ return String(cString: cString)
286
+ }
287
+
288
+ /// Retrieves the benchmark information from the conversation.
289
+ ///
290
+ /// - Returns: The benchmark information
291
+ /// - Throws: A `LiteRTLMError` if the benchmark flag is not enabled or info is unavailable.
292
+ public func getBenchmarkInfo() throws -> BenchmarkInfo {
293
+ let handle = try checkIsAlive()
294
+
295
+ if !ExperimentalFlags.enableBenchmark {
296
+ throw LiteRTLMError.conversation(.benchmarkNotEnabled)
297
+ }
298
+
299
+ guard let benchmarkInfoPtr = litert_lm_conversation_get_benchmark_info(handle) else {
300
+ throw LiteRTLMError.conversation(.benchmarkInfoUnavailable)
301
+ }
302
+ defer { litert_lm_benchmark_info_delete(benchmarkInfoPtr) }
303
+
304
+ let numPrefillTurns = litert_lm_benchmark_info_get_num_prefill_turns(benchmarkInfoPtr)
305
+ let numDecodeTurns = litert_lm_benchmark_info_get_num_decode_turns(benchmarkInfoPtr)
306
+
307
+ let initTimeInSecond = litert_lm_benchmark_info_get_total_init_time_in_second(benchmarkInfoPtr)
308
+ let timeToFirstTokenInSecond = litert_lm_benchmark_info_get_time_to_first_token(
309
+ benchmarkInfoPtr)
310
+
311
+ let lastPrefillTokenCount: Int =
312
+ numPrefillTurns > 0
313
+ ? Int(
314
+ litert_lm_benchmark_info_get_prefill_token_count_at(
315
+ benchmarkInfoPtr, numPrefillTurns - 1)) : 0
316
+ let lastPrefillTokensPerSec: Double =
317
+ numPrefillTurns > 0
318
+ ? litert_lm_benchmark_info_get_prefill_tokens_per_sec_at(
319
+ benchmarkInfoPtr, numPrefillTurns - 1) : 0.0
320
+
321
+ let lastDecodeTokenCount: Int =
322
+ numDecodeTurns > 0
323
+ ? Int(
324
+ litert_lm_benchmark_info_get_decode_token_count_at(
325
+ benchmarkInfoPtr, numDecodeTurns - 1)) : 0
326
+ let lastDecodeTokensPerSec: Double =
327
+ numDecodeTurns > 0
328
+ ? litert_lm_benchmark_info_get_decode_tokens_per_sec_at(
329
+ benchmarkInfoPtr, numDecodeTurns - 1) : 0.0
330
+
331
+ return BenchmarkInfo(
332
+ initTimeInSecond: initTimeInSecond,
333
+ timeToFirstTokenInSecond: timeToFirstTokenInSecond,
334
+ lastPrefillTokenCount: lastPrefillTokenCount,
335
+ lastDecodeTokenCount: lastDecodeTokenCount,
336
+ lastPrefillTokensPerSecond: lastPrefillTokensPerSec,
337
+ lastDecodeTokensPerSecond: lastDecodeTokensPerSec
338
+ )
339
+ }
340
+
341
+ /// Internal Helper Function to convert a JSON string to a `Message`.
342
+ ///
343
+ /// - Parameter jsonString: The JSON string to convert.
344
+ /// - Returns: The `Message` representation of the JSON string.
345
+ /// - Throws: `LiteRTLMError` if the JSON string is invalid.
346
+ public static func jsonToMessage(_ jsonString: String) throws -> Message {
347
+ guard let data = jsonString.data(using: .utf8),
348
+ let jsonObject = try JSONSerialization.jsonObject(with: data) as? [String: Any]
349
+ else {
350
+ throw LiteRTLMError.message(.failedToConvertToJson)
351
+ }
352
+
353
+ var contents: [Content] = []
354
+ if let contentArray = jsonObject["content"] as? [[String: Any]] {
355
+ for item in contentArray {
356
+ if let type = item["type"] as? String, type == "text", let text = item["text"] as? String {
357
+ contents.append(.text(text))
358
+ }
359
+ }
360
+ }
361
+
362
+ var channels: [String: String] = [:]
363
+ if let channelsDict = jsonObject["channels"] as? [String: Any] {
364
+ for (key, value) in channelsDict {
365
+ if let strValue = value as? String {
366
+ channels[key] = strValue
367
+ }
368
+ }
369
+ }
370
+
371
+ if contents.isEmpty && channels.isEmpty {
372
+ throw LiteRTLMError.message(.invalidContent)
373
+ }
374
+
375
+ return Message(contents: contents, channels: channels)
376
+ }
377
+
378
+ /// Context object to bridge the C callback to the Swift AsyncThrowingStream.
379
+ class StreamContext {
380
+ let continuation: AsyncThrowingStream<Message, Error>.Continuation
381
+ let conversation: Conversation
382
+ var toolCallCount: Int = 0
383
+ var pendingToolCalls: [[String: Any]] = []
384
+
385
+ init(continuation: AsyncThrowingStream<Message, Error>.Continuation, conversation: Conversation)
386
+ {
387
+ self.continuation = continuation
388
+ self.conversation = conversation
389
+ }
390
+ }
391
+ }
392
+
393
+ /// A callback function to bridge the C callback to the Swift AsyncThrowingStream.
394
+ private func streamCallback(
395
+ userData: UnsafeMutableRawPointer?,
396
+ responseJson: UnsafePointer<CChar>?,
397
+ isFinal: Bool,
398
+ errorMessage: UnsafePointer<CChar>?
399
+ ) {
400
+ guard let userData = userData else { return }
401
+
402
+ let context = Unmanaged<Conversation.StreamContext>.fromOpaque(userData).takeUnretainedValue()
403
+
404
+ if let errorMessage = errorMessage {
405
+ let errorString = String(cString: errorMessage)
406
+ let error = LiteRTLMError.conversation(.invalidResponse(errorString))
407
+ context.continuation.finish(throwing: error)
408
+
409
+ Unmanaged<Conversation.StreamContext>.fromOpaque(userData).release()
410
+ return
411
+ }
412
+
413
+ if let responseJson = responseJson {
414
+ let responseString = String(cString: responseJson)
415
+ do {
416
+ guard let responseData = responseString.data(using: .utf8),
417
+ let jsonObject = try JSONSerialization.jsonObject(with: responseData) as? [String: Any]
418
+ else {
419
+ throw LiteRTLMError.conversation(.invalidJson("Invalid JSON chunk"))
420
+ }
421
+
422
+ if let toolCalls = jsonObject["tool_calls"] as? [[String: Any]] {
423
+ context.pendingToolCalls.append(contentsOf: toolCalls)
424
+ }
425
+
426
+ if jsonObject["content"] != nil || jsonObject["channels"] != nil {
427
+ let message = try Conversation.jsonToMessage(responseString)
428
+ context.continuation.yield(message)
429
+ }
430
+ } catch {
431
+ logger.error("Failed to parse response JSON: \(error.localizedDescription)")
432
+ context.continuation.finish(throwing: error)
433
+ Unmanaged<Conversation.StreamContext>.fromOpaque(userData).release()
434
+ return
435
+ }
436
+ }
437
+
438
+ if isFinal {
439
+ if !context.pendingToolCalls.isEmpty {
440
+ if context.toolCallCount >= recurringToolCallLimit {
441
+ context.continuation.finish(
442
+ throwing: LiteRTLMError.conversation(
443
+ .recurringToolCallLimitExceeded(limit: recurringToolCallLimit)))
444
+ Unmanaged<Conversation.StreamContext>.fromOpaque(userData).release()
445
+ return
446
+ }
447
+
448
+ context.toolCallCount += 1
449
+ let toolCalls = context.pendingToolCalls
450
+ context.pendingToolCalls = []
451
+
452
+ Task {
453
+ do {
454
+ let toolResponseJson = try await context.conversation.handleToolCalls(toolCalls)
455
+ try context.conversation.sendToStream(
456
+ handle: context.conversation.checkIsAlive(),
457
+ messageJson: toolResponseJson,
458
+ context: context
459
+ )
460
+ } catch {
461
+ context.continuation.finish(throwing: error)
462
+ }
463
+ // Release the reference for the current (finished) call.
464
+ // The new call from sendToStream created its own retained reference.
465
+ Unmanaged<Conversation.StreamContext>.fromOpaque(userData).release()
466
+ }
467
+ } else {
468
+ context.continuation.finish()
469
+ Unmanaged<Conversation.StreamContext>.fromOpaque(userData).release()
470
+ }
471
+ }
472
+ }
@@ -0,0 +1,208 @@
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
+ import CLiteRTLM
18
+
19
+ /// Manages the lifecycle of a LiteRT-LM engine, providing an interface for interacting with the
20
+ /// underlying native library.
21
+ ///
22
+ /// Example usage:
23
+ /// ```
24
+ /// let config = try EngineConfig(modelPath: "...")
25
+ /// let engine = Engine(engineConfig: config)
26
+ /// try await engine.initialize()
27
+ /// ```
28
+ public actor Engine {
29
+ private let logger = Logger(
30
+ subsystem: "com.google.odml.litertlm.swift",
31
+ category: "Engine"
32
+ )
33
+
34
+ /// The configuration for the engine.
35
+ public let engineConfig: EngineConfig
36
+
37
+ /// The native handle to the LiteRT-LM engine. A non-nil value indicates an initialized engine.
38
+ private var handle: OpaquePointer? = nil
39
+
40
+ /// - Parameter engineConfig: The configuration for the engine.
41
+ public init(engineConfig: EngineConfig) {
42
+ self.engineConfig = engineConfig
43
+ }
44
+
45
+ /// Returns `true` if the engine is initialized and ready for use; `false` otherwise.
46
+ public func isInitialized() -> Bool {
47
+ return handle != nil
48
+ }
49
+
50
+ /// Initializes the native LiteRT-LM engine.
51
+ ///
52
+ /// **Note:** This operation can take a significant amount of time (e.g., 10 seconds) depending on
53
+ /// the model size and device hardware. It is strongly recommended to call this method on a
54
+ /// background thread to avoid blocking the main thread.
55
+ ///
56
+ /// - Throws: A `LiteRTLMError` if the engine fails to initialize.
57
+ public func initialize() throws {
58
+ try initializeInternal(benchmarkPrefillTokens: nil, benchmarkDecodeTokens: nil)
59
+ }
60
+
61
+ /// Initializes the native LiteRT-LM engine specifically for benchmarking, avoiding global state params.
62
+ func initializeForBenchmark(prefillTokens: Int, decodeTokens: Int) throws {
63
+ try initializeInternal(
64
+ benchmarkPrefillTokens: prefillTokens, benchmarkDecodeTokens: decodeTokens)
65
+ }
66
+
67
+ private func initializeInternal(benchmarkPrefillTokens: Int?, benchmarkDecodeTokens: Int?) throws
68
+ {
69
+ if isInitialized() {
70
+ throw LiteRTLMError.engine(.alreadyInitialized)
71
+ }
72
+
73
+ // Convert the enums to strings for passing to the native library.
74
+ let backendStr = engineConfig.backend.rawValue
75
+ let visionBackendStr = engineConfig.visionBackend?.rawValue
76
+ let audioBackendStr = engineConfig.audioBackend?.rawValue
77
+
78
+ let settings = litert_lm_engine_settings_create(
79
+ engineConfig.modelPath, backendStr, visionBackendStr, audioBackendStr)
80
+
81
+ guard let settings else {
82
+ throw LiteRTLMError.engine(.failedToCreateSettings)
83
+ }
84
+
85
+ defer { litert_lm_engine_settings_delete(settings) }
86
+
87
+ if let maxNumTokens = engineConfig.maxNumTokens {
88
+ litert_lm_engine_settings_set_max_num_tokens(settings, Int32(maxNumTokens))
89
+ }
90
+ if let cacheDir = engineConfig.cacheDir {
91
+ litert_lm_engine_settings_set_cache_dir(settings, cacheDir)
92
+ }
93
+ if let prefill = benchmarkPrefillTokens, let decode = benchmarkDecodeTokens {
94
+ litert_lm_engine_settings_enable_benchmark(settings)
95
+ litert_lm_engine_settings_set_num_prefill_tokens(settings, Int32(prefill))
96
+ litert_lm_engine_settings_set_num_decode_tokens(settings, Int32(decode))
97
+ } else if ExperimentalFlags.enableBenchmark {
98
+ litert_lm_engine_settings_enable_benchmark(settings)
99
+ }
100
+ if let enableSpeculativeDecoding = ExperimentalFlags.enableSpeculativeDecoding {
101
+ litert_lm_engine_settings_set_enable_speculative_decoding(settings, enableSpeculativeDecoding)
102
+ }
103
+
104
+ guard let engine = litert_lm_engine_create(settings) else {
105
+ throw LiteRTLMError.engine(.failedToCreateEngine)
106
+ }
107
+
108
+ self.handle = engine
109
+ }
110
+
111
+ /// Creates a new `Conversation` from the initialized engine.
112
+ ///
113
+ /// - Parameter ConversationConfig: The configuration for the conversation.
114
+ /// - Returns: `Conversation` The created conversation.
115
+ /// - Throws: A `LiteRTLMError` if the conversation creation fails.
116
+ ///
117
+ public func createConversation(with config: ConversationConfig? = nil) throws -> Conversation {
118
+ guard isInitialized() else {
119
+ throw LiteRTLMError.engine(.notInitialized)
120
+ }
121
+
122
+ // We can force unwrap handle here because the engine is guaranteed to be initialized, and
123
+ // initialization will set the handle.
124
+ let engineHandle = self.handle!
125
+
126
+ let conversationConfig = config ?? ConversationConfig()
127
+
128
+ let systemMessage = conversationConfig.systemMessage
129
+ let initialSystemMessageCount = conversationConfig.initialMessages.filter { $0.role == .system }
130
+ .count
131
+
132
+ if systemMessage != nil && initialSystemMessageCount > 0 {
133
+ throw LiteRTLMError.config(.multipleSystemMessages)
134
+ }
135
+ if initialSystemMessageCount > 1 {
136
+ throw LiteRTLMError.config(.multipleSystemMessages)
137
+ }
138
+
139
+ let toolManager = ToolManager(tools: conversationConfig.tools)
140
+
141
+ let systemMessageJsonStr = (try? conversationConfig.systemMessage?.contents.jsonString) ?? ""
142
+ let toolDescriptionJsonStr = toolManager.toolsJsonDescription
143
+
144
+ let initialMessagesJson = conversationConfig.initialMessages.map { $0.toJson }
145
+ let messagesJsonStr: String
146
+ if !initialMessagesJson.isEmpty,
147
+ let messagesData = try? JSONSerialization.data(
148
+ withJSONObject: initialMessagesJson, options: []),
149
+ let serializedStr = String(data: messagesData, encoding: .utf8)
150
+ {
151
+ messagesJsonStr = serializedStr
152
+ } else {
153
+ messagesJsonStr = ""
154
+ }
155
+
156
+ let cSessionConfig = litert_lm_session_config_create()
157
+ guard let cSessionConfig else {
158
+ throw LiteRTLMError.engine(.failedToCreateSessionConfig)
159
+ }
160
+ defer { litert_lm_session_config_delete(cSessionConfig) }
161
+
162
+ if let samplerParams = conversationConfig.samplerConfig {
163
+ var params = LiteRtLmSamplerParams(
164
+ // Based on the current engine implementation, when SamplerConfig is set, we must switch to
165
+ // the topP sampling type.
166
+ type: kLiteRtLmSamplerTypeTopP,
167
+ top_k: Int32(samplerParams.topK),
168
+ top_p: samplerParams.topP,
169
+ temperature: samplerParams.temperature,
170
+ seed: Int32(samplerParams.seed)
171
+ )
172
+ litert_lm_session_config_set_sampler_params(cSessionConfig, &params)
173
+ }
174
+
175
+ guard let cConversationConfig = litert_lm_conversation_config_create() else {
176
+ throw LiteRTLMError.engine(.failedToCreateConversationConfig)
177
+ }
178
+ defer { litert_lm_conversation_config_delete(cConversationConfig) }
179
+
180
+ litert_lm_conversation_config_set_session_config(cConversationConfig, cSessionConfig)
181
+ if !systemMessageJsonStr.isEmpty {
182
+ litert_lm_conversation_config_set_system_message(cConversationConfig, systemMessageJsonStr)
183
+ }
184
+ if !toolDescriptionJsonStr.isEmpty {
185
+ litert_lm_conversation_config_set_tools(cConversationConfig, toolDescriptionJsonStr)
186
+ }
187
+ if !messagesJsonStr.isEmpty {
188
+ litert_lm_conversation_config_set_messages(cConversationConfig, messagesJsonStr)
189
+ }
190
+ litert_lm_conversation_config_set_enable_constrained_decoding(
191
+ cConversationConfig, ExperimentalFlags.enableConversationConstrainedDecoding)
192
+
193
+ guard
194
+ let conversationHandle = litert_lm_conversation_create(
195
+ engineHandle, cConversationConfig)
196
+ else {
197
+ throw LiteRTLMError.engine(.failedToCreateConversation)
198
+ }
199
+
200
+ return Conversation(handle: conversationHandle, toolManager: toolManager)
201
+ }
202
+
203
+ deinit {
204
+ if let handle = handle {
205
+ litert_lm_engine_delete(handle)
206
+ }
207
+ }
208
+ }