| 1 | import Foundation |
| 2 | |
| 3 | /// A capability your app makes available to the local model. |
| 4 | /// |
| 5 | /// Ed never invents native capabilities. Your app registers each tool and |
| 6 | /// remains responsible for executing it. |
| 7 | public protocol EdTool: Sendable { |
| 8 | var definition: EdToolDefinition { get } |
| 9 | |
| 10 | /// Executes the tool with the JSON object produced by the model. |
| 11 | func execute(arguments: String) async throws -> String |
| 12 | } |
| 13 | |
| 14 | public enum EdToolRisk: Sendable { |
| 15 | /// Safe to run without asking, such as reading app state. |
| 16 | case readOnly |
| 17 | |
| 18 | /// Changes state and must be approved first, such as saving or deleting. |
| 19 | case mutating |
| 20 | } |
| 21 | |
| 22 | public struct EdToolDefinition: Sendable { |
| 23 | public let name: String |
| 24 | public let description: String |
| 25 | public let parametersSchema: String |
| 26 | public let risk: EdToolRisk |
| 27 | |
| 28 | public init( |
| 29 | name: String, |
| 30 | description: String, |
| 31 | parametersSchema: String, |
| 32 | risk: EdToolRisk |
| 33 | ) { |
| 34 | self.name = name |
| 35 | self.description = description |
| 36 | self.parametersSchema = parametersSchema |
| 37 | self.risk = risk |
| 38 | } |
| 39 | |
| 40 | public static func readOnly( |
| 41 | name: String, |
| 42 | description: String, |
| 43 | parametersSchema: String |
| 44 | ) -> Self { |
| 45 | Self( |
| 46 | name: name, |
| 47 | description: description, |
| 48 | parametersSchema: parametersSchema, |
| 49 | risk: .readOnly |
| 50 | ) |
| 51 | } |
| 52 | |
| 53 | public static func mutating( |
| 54 | name: String, |
| 55 | description: String, |
| 56 | parametersSchema: String |
| 57 | ) -> Self { |
| 58 | Self( |
| 59 | name: name, |
| 60 | description: description, |
| 61 | parametersSchema: parametersSchema, |
| 62 | risk: .mutating |
| 63 | ) |
| 64 | } |
| 65 | } |
| 66 | |
| 67 | public struct EdToolCall: Sendable, Equatable { |
| 68 | public let id: String |
| 69 | public let name: String |
| 70 | public let arguments: String |
| 71 | } |
| 72 | |
| 73 | public enum EdApprovalDecision: Sendable { |
| 74 | case allowOnce |
| 75 | case allowForSession |
| 76 | case deny |
| 77 | } |
| 78 | |
| 79 | public typealias EdApprovalHandler = @Sendable (EdToolCall) async -> EdApprovalDecision |
| 80 | |
| 81 | /// Like ``EdApprovalHandler``, and also told the tool's risk, so the approval |
| 82 | /// sheet can say whether the call changes anything. |
| 83 | public typealias EdRiskApprovalHandler = |
| 84 | @Sendable (EdToolCall, EdToolRisk) async -> EdApprovalDecision |
| 85 | |
| 86 | public struct EdAgentConfiguration: Sendable { |
| 87 | public var maxToolRounds: UInt8 |
| 88 | public var maxToolOutputCharacters: UInt64 |
| 89 | |
| 90 | public init(maxToolRounds: UInt8 = 8, maxToolOutputCharacters: UInt64 = 10_000) { |
| 91 | self.maxToolRounds = maxToolRounds |
| 92 | self.maxToolOutputCharacters = maxToolOutputCharacters |
| 93 | } |
| 94 | } |
| 95 | |
| 96 | public struct EdAgentReply: Sendable, Equatable { |
| 97 | public let text: String |
| 98 | public let durationSeconds: Double |
| 99 | public let duration: String |
| 100 | public let toolRounds: UInt8 |
| 101 | } |
| 102 | |
| 103 | public struct EdReply: Sendable, Equatable { |
| 104 | public let text: String |
| 105 | public let duration: String |
| 106 | } |
| 107 | |
| 108 | public enum EdEngineStatus: Sendable, Equatable { |
| 109 | case unloaded |
| 110 | case loading |
| 111 | case ready |
| 112 | case generating |
| 113 | case error |
| 114 | } |
| 115 | |
| 116 | public struct EdEngineInfo: Sendable, Equatable { |
| 117 | public let status: EdEngineStatus |
| 118 | public let modelName: String? |
| 119 | public let approximateMemory: String? |
| 120 | public let historyLength: UInt64 |
| 121 | |
| 122 | public init( |
| 123 | status: EdEngineStatus, |
| 124 | modelName: String? = nil, |
| 125 | approximateMemory: String? = nil, |
| 126 | historyLength: UInt64 = 0 |
| 127 | ) { |
| 128 | self.status = status |
| 129 | self.modelName = modelName |
| 130 | self.approximateMemory = approximateMemory |
| 131 | self.historyLength = historyLength |
| 132 | } |
| 133 | } |
| 134 | |
| 135 | public enum EdChatRole: Sendable, Equatable { |
| 136 | case system |
| 137 | case user |
| 138 | case assistant |
| 139 | } |
| 140 | |
| 141 | public struct EdChatMessage: Sendable, Equatable { |
| 142 | public let role: EdChatRole |
| 143 | public let content: String |
| 144 | |
| 145 | public init(role: EdChatRole, content: String) { |
| 146 | self.role = role |
| 147 | self.content = content |
| 148 | } |
| 149 | } |
| 150 | |
| 151 | public enum EdEvent: Sendable { |
| 152 | case statusChanged(status: EdEngineStatus, modelName: String?, error: String?) |
| 153 | |
| 154 | /// A piece of the reply as it is generated during ``EdAgent/run(_:)``. |
| 155 | case textDelta(String) |
| 156 | case toolRequested(EdToolCall) |
| 157 | case approvalRequested(EdToolCall) |
| 158 | case toolStarted(EdToolCall) |
| 159 | case toolFinished(callID: String, content: String, isError: Bool) |
| 160 | case agentReplied(EdAgentReply) |
| 161 | case warning(String) |
| 162 | } |
| 163 | |
| 164 | public struct EdSamplingConfiguration: Sendable { |
| 165 | public var temperature: Double? |
| 166 | public var topP: Double? |
| 167 | public var topK: UInt64? |
| 168 | public var minP: Double? |
| 169 | public var maxTokens: UInt64? |
| 170 | public var frequencyPenalty: Float? |
| 171 | public var presencePenalty: Float? |
| 172 | |
| 173 | public init( |
| 174 | temperature: Double? = nil, |
| 175 | topP: Double? = nil, |
| 176 | topK: UInt64? = nil, |
| 177 | minP: Double? = nil, |
| 178 | maxTokens: UInt64? = nil, |
| 179 | frequencyPenalty: Float? = nil, |
| 180 | presencePenalty: Float? = nil |
| 181 | ) { |
| 182 | self.temperature = temperature |
| 183 | self.topP = topP |
| 184 | self.topK = topK |
| 185 | self.minP = minP |
| 186 | self.maxTokens = maxTokens |
| 187 | self.frequencyPenalty = frequencyPenalty |
| 188 | self.presencePenalty = presencePenalty |
| 189 | } |
| 190 | } |
| 191 | |
| 192 | public struct EdGGUFModelConfiguration: Sendable { |
| 193 | public var modelID: String |
| 194 | public var files: [String] |
| 195 | public var tokenizerModelID: String? |
| 196 | public var displayName: String |
| 197 | public var approximateMemory: String |
| 198 | public var chatTemplate: String? |
| 199 | |
| 200 | public init( |
| 201 | modelID: String, |
| 202 | files: [String], |
| 203 | tokenizerModelID: String? = nil, |
| 204 | displayName: String, |
| 205 | approximateMemory: String, |
| 206 | chatTemplate: String? = nil |
| 207 | ) { |
| 208 | self.modelID = modelID |
| 209 | self.files = files |
| 210 | self.tokenizerModelID = tokenizerModelID |
| 211 | self.displayName = displayName |
| 212 | self.approximateMemory = approximateMemory |
| 213 | self.chatTemplate = chatTemplate |
| 214 | } |
| 215 | } |
| 216 | |
| 217 | public struct EdUQFFModelConfiguration: Sendable { |
| 218 | public var modelID: String |
| 219 | public var files: [String] |
| 220 | public var displayName: String |
| 221 | public var approximateMemory: String |
| 222 | public var chatTemplate: String? |
| 223 | |
| 224 | public init( |
| 225 | modelID: String, |
| 226 | files: [String], |
| 227 | displayName: String, |
| 228 | approximateMemory: String, |
| 229 | chatTemplate: String? = nil |
| 230 | ) { |
| 231 | self.modelID = modelID |
| 232 | self.files = files |
| 233 | self.displayName = displayName |
| 234 | self.approximateMemory = approximateMemory |
| 235 | self.chatTemplate = chatTemplate |
| 236 | } |
| 237 | } |
| 238 | |
| 239 | public enum EdEnvironment: Sendable { |
| 240 | case development |
| 241 | case production |
| 242 | } |
| 243 | |
| 244 | /// A local agent powered by Onde. |
| 245 | /// |
| 246 | /// The actor protects model history, the tool registry, and inference so your |
| 247 | /// UI can call it safely from Swift concurrency. |
| 248 | public actor EdAgent { |
| 249 | private let core: FfiEdAgent |
| 250 | private let tools: ToolDispatcher |
| 251 | private let approvals: ApprovalAdapter |
| 252 | private nonisolated let eventHub: EventHub |
| 253 | |
| 254 | /// - Parameter appID: Your Onde app id, so the engine reports usage under |
| 255 | /// it. Pass `nil` for none. |
| 256 | public init( |
| 257 | appID: String?, |
| 258 | configuration: EdAgentConfiguration = .init(), |
| 259 | approvalHandler: @escaping EdRiskApprovalHandler = { _, _ in .deny } |
| 260 | ) { |
| 261 | let tools = ToolDispatcher() |
| 262 | let approvals = ApprovalAdapter(handler: approvalHandler) |
| 263 | let eventHub = EventHub() |
| 264 | |
| 265 | self.tools = tools |
| 266 | self.approvals = approvals |
| 267 | self.eventHub = eventHub |
| 268 | self.core = FfiEdAgent.newWithAppId( |
| 269 | executor: tools, |
| 270 | approvals: approvals, |
| 271 | events: eventHub, |
| 272 | config: FfiAgentConfig( |
| 273 | maxToolRounds: configuration.maxToolRounds, |
| 274 | maxToolOutputChars: configuration.maxToolOutputCharacters |
| 275 | ), |
| 276 | appId: appID |
| 277 | ) |
| 278 | } |
| 279 | |
| 280 | public init( |
| 281 | configuration: EdAgentConfiguration = .init(), |
| 282 | approvalHandler: @escaping EdApprovalHandler = { _ in .deny } |
| 283 | ) { |
| 284 | self.init( |
| 285 | appID: nil, |
| 286 | configuration: configuration, |
| 287 | approvalHandler: { call, _ in await approvalHandler(call) } |
| 288 | ) |
| 289 | } |
| 290 | |
| 291 | /// A separate stream for each observer. Stop its task to unsubscribe. |
| 292 | public nonisolated func events() -> AsyncStream<EdEvent> { |
| 293 | eventHub.makeStream() |
| 294 | } |
| 295 | |
| 296 | public func register(_ tool: any EdTool) async throws { |
| 297 | try await core.registerTool(tool: tool.definition.ffi) |
| 298 | tools.set(tool) |
| 299 | } |
| 300 | |
| 301 | @discardableResult |
| 302 | public func unregisterTool(named name: String) async -> Bool { |
| 303 | let removed = await core.unregisterTool(name: name) |
| 304 | if removed { |
| 305 | tools.remove(named: name) |
| 306 | } |
| 307 | return removed |
| 308 | } |
| 309 | |
| 310 | public func removeAllTools() async { |
| 311 | await core.removeAllTools() |
| 312 | tools.removeAll() |
| 313 | } |
| 314 | |
| 315 | /// Runs the full agent loop: model -> tool calls -> tool results -> model. |
| 316 | public func run(_ message: String) async throws -> EdAgentReply { |
| 317 | try await withTaskCancellationHandler { |
| 318 | try await core.run(message: message).swift |
| 319 | } onCancel: { |
| 320 | core.cancel() |
| 321 | } |
| 322 | } |
| 323 | |
| 324 | /// Sends one ordinary chat turn without exposing tools to the model. |
| 325 | public func send(_ message: String) async throws -> EdReply { |
| 326 | let reply = try await core.send(message: message) |
| 327 | return EdReply(text: reply.text, duration: reply.duration) |
| 328 | } |
| 329 | |
| 330 | /// Streams the reply to one ordinary chat turn, without exposing tools. |
| 331 | /// |
| 332 | /// Yields text deltas as they are generated. Cancelling the consuming task |
| 333 | /// (or breaking out of the loop) stops generation; what was produced stays |
| 334 | /// in the history. |
| 335 | public nonisolated func stream(_ message: String) -> AsyncThrowingStream<String, Error> { |
| 336 | let core = self.core |
| 337 | return AsyncThrowingStream { continuation in |
| 338 | let task = Task { |
| 339 | do { |
| 340 | try await core.streamMessage( |
| 341 | message: message, |
| 342 | listener: StreamAdapter(continuation: continuation) |
| 343 | ) |
| 344 | continuation.finish() |
| 345 | } catch { |
| 346 | continuation.finish(throwing: error) |
| 347 | } |
| 348 | } |
| 349 | continuation.onTermination = { _ in |
| 350 | core.cancelStream() |
| 351 | task.cancel() |
| 352 | } |
| 353 | } |
| 354 | } |
| 355 | |
| 356 | /// One-shot generation over `messages`. The conversation is left alone. |
| 357 | public func generate( |
| 358 | messages: [EdChatMessage], |
| 359 | sampling: EdSamplingConfiguration? = nil |
| 360 | ) async throws -> String { |
| 361 | try await core.generate(messages: messages.map(\.ffi), sampling: sampling?.ffi) |
| 362 | } |
| 363 | |
| 364 | /// Replaces the conversation with `messages`, for switching to a saved chat. |
| 365 | /// Needs a loaded model. Also revokes any "allow for session" approvals. |
| 366 | public func restoreHistory(_ messages: [EdChatMessage]) async { |
| 367 | await core.restoreHistory(messages: messages.map(\.ffi)) |
| 368 | } |
| 369 | |
| 370 | /// Sets the system prompt for later turns. Needs a loaded model. |
| 371 | public func setSystemPrompt(_ prompt: String) async { |
| 372 | await core.setSystemPrompt(prompt: prompt) |
| 373 | } |
| 374 | |
| 375 | public func clearSystemPrompt() async { |
| 376 | await core.clearSystemPrompt() |
| 377 | } |
| 378 | |
| 379 | public func setSampling(_ sampling: EdSamplingConfiguration) async { |
| 380 | await core.setSampling(sampling: sampling.ffi) |
| 381 | } |
| 382 | |
| 383 | /// Loads a small tool-capable model chosen for the current Apple device. |
| 384 | @discardableResult |
| 385 | public func loadDefaultAgentModel(systemPrompt: String? = nil) async throws -> Double { |
| 386 | try await core.loadDefaultAgentModel(systemPrompt: systemPrompt) |
| 387 | } |
| 388 | |
| 389 | @discardableResult |
| 390 | public func load( |
| 391 | gguf config: EdGGUFModelConfiguration, |
| 392 | systemPrompt: String? = nil, |
| 393 | sampling: EdSamplingConfiguration? = nil |
| 394 | ) async throws -> Double { |
| 395 | try await core.loadGgufModel( |
| 396 | config: config.ffi, |
| 397 | systemPrompt: systemPrompt, |
| 398 | sampling: sampling?.ffi |
| 399 | ) |
| 400 | } |
| 401 | |
| 402 | @discardableResult |
| 403 | public func load( |
| 404 | uqff config: EdUQFFModelConfiguration, |
| 405 | systemPrompt: String? = nil, |
| 406 | sampling: EdSamplingConfiguration? = nil |
| 407 | ) async throws -> Double { |
| 408 | try await core.loadUqffModel( |
| 409 | config: config.ffi, |
| 410 | systemPrompt: systemPrompt, |
| 411 | sampling: sampling?.ffi |
| 412 | ) |
| 413 | } |
| 414 | |
| 415 | @discardableResult |
| 416 | public func loadAssignedModel( |
| 417 | environment: EdEnvironment, |
| 418 | appID: String, |
| 419 | appSecret: String, |
| 420 | systemPrompt: String? = nil, |
| 421 | sampling: EdSamplingConfiguration? = nil |
| 422 | ) async throws -> Double { |
| 423 | try await core.loadAssignedModel( |
| 424 | environment: environment.ffi, |
| 425 | appId: appID, |
| 426 | appSecret: appSecret, |
| 427 | systemPrompt: systemPrompt, |
| 428 | sampling: sampling?.ffi |
| 429 | ) |
| 430 | } |
| 431 | |
| 432 | public nonisolated func cancel() { |
| 433 | core.cancel() |
| 434 | } |
| 435 | |
| 436 | @discardableResult |
| 437 | public func unload() async -> String? { |
| 438 | await core.unload() |
| 439 | } |
| 440 | |
| 441 | public func isLoaded() async -> Bool { |
| 442 | await core.isLoaded() |
| 443 | } |
| 444 | |
| 445 | public func info() async -> EdEngineInfo { |
| 446 | let info = await core.info() |
| 447 | return EdEngineInfo( |
| 448 | status: info.status.swift, |
| 449 | modelName: info.modelName, |
| 450 | approximateMemory: info.approxMemory, |
| 451 | historyLength: info.historyLength |
| 452 | ) |
| 453 | } |
| 454 | |
| 455 | public func history() async -> [EdChatMessage] { |
| 456 | await core.history().map(\.swift) |
| 457 | } |
| 458 | |
| 459 | @discardableResult |
| 460 | public func clearHistory() async -> UInt64 { |
| 461 | await core.clearHistory() |
| 462 | } |
| 463 | |
| 464 | public static func configureCacheDirectory(_ url: URL) { |
| 465 | configureCacheDir(path: url.path) |
| 466 | } |
| 467 | |
| 468 | public static func defaultAgentModelConfiguration() -> EdGGUFModelConfiguration { |
| 469 | defaultAgentModelConfig().swift |
| 470 | } |
| 471 | } |
| 472 | |
| 473 | private final class ToolDispatcher: FfiToolExecutor, @unchecked Sendable { |
| 474 | private let lock = NSLock() |
| 475 | private var tools: [String: any EdTool] = [:] |
| 476 | |
| 477 | func set(_ tool: any EdTool) { |
| 478 | lock.withLock { tools[tool.definition.name] = tool } |
| 479 | } |
| 480 | |
| 481 | func remove(named name: String) { |
| 482 | lock.withLock { _ = tools.removeValue(forKey: name) } |
| 483 | } |
| 484 | |
| 485 | func removeAll() { |
| 486 | lock.withLock { tools.removeAll() } |
| 487 | } |
| 488 | |
| 489 | func execute(toolName: String, arguments: String) async -> FfiToolOutput { |
| 490 | let tool = lock.withLock { tools[toolName] } |
| 491 | guard let tool else { |
| 492 | return FfiToolOutput(content: "", error: "Unknown tool: \(toolName)") |
| 493 | } |
| 494 | |
| 495 | do { |
| 496 | return FfiToolOutput( |
| 497 | content: try await tool.execute(arguments: arguments), |
| 498 | error: nil |
| 499 | ) |
| 500 | } catch { |
| 501 | return FfiToolOutput(content: "", error: String(describing: error)) |
| 502 | } |
| 503 | } |
| 504 | } |
| 505 | |
| 506 | private final class ApprovalAdapter: FfiApprovalHandler, @unchecked Sendable { |
| 507 | private let handler: EdRiskApprovalHandler |
| 508 | |
| 509 | init(handler: @escaping EdRiskApprovalHandler) { |
| 510 | self.handler = handler |
| 511 | } |
| 512 | |
| 513 | func approve(request: FfiApprovalRequest) async -> FfiApprovalDecision { |
| 514 | await handler(request.call.swift, request.risk.swift).ffi |
| 515 | } |
| 516 | } |
| 517 | |
| 518 | private final class StreamAdapter: FfiStreamListener, @unchecked Sendable { |
| 519 | private let continuation: AsyncThrowingStream<String, Error>.Continuation |
| 520 | |
| 521 | init(continuation: AsyncThrowingStream<String, Error>.Continuation) { |
| 522 | self.continuation = continuation |
| 523 | } |
| 524 | |
| 525 | func onDelta(delta: String) { |
| 526 | continuation.yield(delta) |
| 527 | } |
| 528 | } |
| 529 | |
| 530 | private final class EventHub: FfiEventListener, @unchecked Sendable { |
| 531 | private let lock = NSLock() |
| 532 | private var continuations: [UUID: AsyncStream<EdEvent>.Continuation] = [:] |
| 533 | |
| 534 | func makeStream() -> AsyncStream<EdEvent> { |
| 535 | let id = UUID() |
| 536 | return AsyncStream { continuation in |
| 537 | lock.withLock { continuations[id] = continuation } |
| 538 | continuation.onTermination = { [weak self] _ in |
| 539 | self?.lock.withLock { _ = self?.continuations.removeValue(forKey: id) } |
| 540 | } |
| 541 | } |
| 542 | } |
| 543 | |
| 544 | func statusChanged(update: FfiStatusUpdate) { |
| 545 | emit(.statusChanged( |
| 546 | status: update.status.swift, |
| 547 | modelName: update.modelName, |
| 548 | error: update.error |
| 549 | )) |
| 550 | } |
| 551 | |
| 552 | func textDelta(delta: String) { emit(.textDelta(delta)) } |
| 553 | func toolRequested(call: FfiToolCall) { emit(.toolRequested(call.swift)) } |
| 554 | func approvalRequested(request: FfiApprovalRequest) { emit(.approvalRequested(request.call.swift)) } |
| 555 | func toolStarted(call: FfiToolCall) { emit(.toolStarted(call.swift)) } |
| 556 | |
| 557 | func toolFinished(toolCallId: String, content: String, isError: Bool) { |
| 558 | emit(.toolFinished(callID: toolCallId, content: content, isError: isError)) |
| 559 | } |
| 560 | |
| 561 | func agentReplied(reply: FfiAgentReply) { emit(.agentReplied(reply.swift)) } |
| 562 | func warning(message: String) { emit(.warning(message)) } |
| 563 | |
| 564 | private func emit(_ event: EdEvent) { |
| 565 | let current = lock.withLock { Array(continuations.values) } |
| 566 | current.forEach { $0.yield(event) } |
| 567 | } |
| 568 | } |
| 569 | |
| 570 | private extension EdToolDefinition { |
| 571 | var ffi: FfiToolDefinition { |
| 572 | FfiToolDefinition( |
| 573 | name: name, |
| 574 | description: description, |
| 575 | parametersSchema: parametersSchema, |
| 576 | risk: risk == .readOnly ? .readOnly : .mutating |
| 577 | ) |
| 578 | } |
| 579 | } |
| 580 | |
| 581 | private extension FfiToolRisk { |
| 582 | var swift: EdToolRisk { self == .readOnly ? .readOnly : .mutating } |
| 583 | } |
| 584 | |
| 585 | private extension EdChatMessage { |
| 586 | var ffi: FfiChatMessage { |
| 587 | let role: FfiChatRole = switch self.role { |
| 588 | case .system: .system |
| 589 | case .user: .user |
| 590 | case .assistant: .assistant |
| 591 | } |
| 592 | return FfiChatMessage(role: role, content: content) |
| 593 | } |
| 594 | } |
| 595 | |
| 596 | private extension FfiToolCall { |
| 597 | var swift: EdToolCall { EdToolCall(id: id, name: name, arguments: arguments) } |
| 598 | } |
| 599 | |
| 600 | private extension EdApprovalDecision { |
| 601 | var ffi: FfiApprovalDecision { |
| 602 | switch self { |
| 603 | case .allowOnce: .allowOnce |
| 604 | case .allowForSession: .allowForSession |
| 605 | case .deny: .deny |
| 606 | } |
| 607 | } |
| 608 | } |
| 609 | |
| 610 | private extension FfiAgentReply { |
| 611 | var swift: EdAgentReply { |
| 612 | EdAgentReply( |
| 613 | text: text, |
| 614 | durationSeconds: durationSeconds, |
| 615 | duration: duration, |
| 616 | toolRounds: toolRounds |
| 617 | ) |
| 618 | } |
| 619 | } |
| 620 | |
| 621 | private extension FfiEngineStatus { |
| 622 | var swift: EdEngineStatus { |
| 623 | switch self { |
| 624 | case .unloaded: .unloaded |
| 625 | case .loading: .loading |
| 626 | case .ready: .ready |
| 627 | case .generating: .generating |
| 628 | case .error: .error |
| 629 | } |
| 630 | } |
| 631 | } |
| 632 | |
| 633 | private extension FfiChatMessage { |
| 634 | var swift: EdChatMessage { |
| 635 | let role: EdChatRole = switch self.role { |
| 636 | case .system: .system |
| 637 | case .user: .user |
| 638 | case .assistant: .assistant |
| 639 | } |
| 640 | return EdChatMessage(role: role, content: content) |
| 641 | } |
| 642 | } |
| 643 | |
| 644 | private extension EdSamplingConfiguration { |
| 645 | var ffi: FfiSamplingConfig { |
| 646 | FfiSamplingConfig( |
| 647 | temperature: temperature, |
| 648 | topP: topP, |
| 649 | topK: topK, |
| 650 | minP: minP, |
| 651 | maxTokens: maxTokens, |
| 652 | frequencyPenalty: frequencyPenalty, |
| 653 | presencePenalty: presencePenalty |
| 654 | ) |
| 655 | } |
| 656 | } |
| 657 | |
| 658 | private extension EdGGUFModelConfiguration { |
| 659 | var ffi: FfiGgufModelConfig { |
| 660 | FfiGgufModelConfig( |
| 661 | modelId: modelID, |
| 662 | files: files, |
| 663 | tokModelId: tokenizerModelID, |
| 664 | displayName: displayName, |
| 665 | approxMemory: approximateMemory, |
| 666 | chatTemplate: chatTemplate |
| 667 | ) |
| 668 | } |
| 669 | } |
| 670 | |
| 671 | private extension FfiGgufModelConfig { |
| 672 | var swift: EdGGUFModelConfiguration { |
| 673 | EdGGUFModelConfiguration( |
| 674 | modelID: modelId, |
| 675 | files: files, |
| 676 | tokenizerModelID: tokModelId, |
| 677 | displayName: displayName, |
| 678 | approximateMemory: approxMemory, |
| 679 | chatTemplate: chatTemplate |
| 680 | ) |
| 681 | } |
| 682 | } |
| 683 | |
| 684 | private extension EdUQFFModelConfiguration { |
| 685 | var ffi: FfiUqffModelConfig { |
| 686 | FfiUqffModelConfig( |
| 687 | modelId: modelID, |
| 688 | files: files, |
| 689 | displayName: displayName, |
| 690 | approxMemory: approximateMemory, |
| 691 | chatTemplate: chatTemplate |
| 692 | ) |
| 693 | } |
| 694 | } |
| 695 | |
| 696 | private extension EdEnvironment { |
| 697 | var ffi: FfiEnvironment { |
| 698 | switch self { |
| 699 | case .development: .development |
| 700 | case .production: .production |
| 701 | } |
| 702 | } |
| 703 | } |