import Foundation /// Container für HuggingFace-Cache, geteilt über alle mana-e.V.- /// Apps via App-Group `group.ev.mana.models`. Apps mit dieser /// Group im Entitlement lesen Modelle aus demselben Pfad → kein /// Doppel-Download. /// /// **Setup pro App:** /// /// 1. Apple-Dev-Portal: App ID öffnen → App Groups Capability → /// `group.ev.mana.models` hinzufügen. /// 2. `project.yml` Entitlement: /// ```yaml /// com.apple.security.application-groups: /// - group.ev.mana. /// - group.ev.mana.models /// ``` /// 3. App-Code beim Boot: /// ```swift /// ManaSharedModels.configureHuggingFaceCacheEnv() /// ``` /// Damit zeigt MLX-Swift's HuggingFace-Hub-Client beim ersten /// `Hub.snapshot(...)` automatisch in den Shared-Container. /// /// SOT-Doku: `mana/docs/MANA_LLM.md`. public enum ManaSharedModels { /// Kanonische App-Group für gemeinsamen Modell-Container. public static let appGroup = "group.ev.mana.models" /// Pfad-Convention im Container, kompatibel zu HuggingFace-Hub- /// Default-Layout (`~/.cache/huggingface/hub/...`). public static let hubSubdirectory = "huggingface/hub" /// URL des HuggingFace-Cache-Roots im Shared-Container. /// /// Returns `nil`, wenn: /// - die App das `group.ev.mana.models`-Entitlement nicht hat, /// - die App in einem Modus läuft, in dem App-Group-Container /// nicht zugänglich sind (z.B. bestimmte Extension-Kontexte). /// /// Caller fallen dann auf App-eigenen `Application Support` /// zurück — `legacyCacheURL()` liefert das. public static func cacheURL() -> URL? { guard let container = FileManager.default.containerURL( forSecurityApplicationGroupIdentifier: appGroup ) else { return nil } let hub = container.appending(path: hubSubdirectory) // Verzeichnis anlegen + von iCloud-Backup ausschließen. try? FileManager.default.createDirectory(at: hub, withIntermediateDirectories: true) var hubVar = hub var values = URLResourceValues() values.isExcludedFromBackup = true try? hubVar.setResourceValues(values) return hub } /// Fallback wenn `cacheURL()` `nil` ist: App-eigener /// `Application Support/huggingface/hub`. Pendant zum bisherigen /// memoro-spezifischen Pfad — kein Sharing, aber funktional. public static func legacyCacheURL() -> URL? { guard let appSupport = try? FileManager.default.url( for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: true ) else { return nil } let hub = appSupport.appending(path: hubSubdirectory) try? FileManager.default.createDirectory(at: hub, withIntermediateDirectories: true) var hubVar = hub var values = URLResourceValues() values.isExcludedFromBackup = true try? hubVar.setResourceValues(values) return hub } /// Bevorzugter Cache-Pfad: shared-Container falls verfügbar, /// sonst Legacy-App-eigener Application Support. public static func effectiveCacheURL() -> URL? { cacheURL() ?? legacyCacheURL() } /// Setzt die `HF_HUB_CACHE`-Environment-Variable auf den shared /// Container. MLX-Swift's `HubClient` und swift-huggingface's /// `Hub.snapshot(...)` lesen diese Variable beim Boot. /// /// Idempotent. Wenn der Shared-Container nicht zugänglich ist, /// wird die Variable auf den Legacy-Pfad gesetzt — Apps müssen /// keinen Fallback-Code schreiben. /// /// **Wichtig:** Diese Funktion möglichst früh im App-Boot /// aufrufen (vor dem ersten LLM-Call), z.B. im /// `init()` der `@main`-App-Struct. @discardableResult public static func configureHuggingFaceCacheEnv() -> URL? { guard let url = effectiveCacheURL() else { return nil } setenv("HF_HUB_CACHE", url.path, 1) return url } /// Liefert URL eines konkreten Modell-Repo-Verzeichnisses im /// Cache. Konvenientes Pendant zum HuggingFace-Pfad-Schema: /// `/models----`. /// /// Beispiel: /// ```swift /// ManaSharedModels.modelDirURL(repo: "mlx-community/gemma-4-e2b-it-4bit") /// // → /models--mlx-community--gemma-4-e2b-it-4bit /// ``` public static func modelDirURL(repo: String) -> URL? { guard let hub = effectiveCacheURL() else { return nil } let dirName = "models--" + repo.replacingOccurrences(of: "/", with: "--") return hub.appending(path: dirName) } /// Best-effort-Größenschätzung des Cache-Inhalts in Bytes. /// Settings-Views können das nutzen, um "Lokale Modelle: 3.8 GB" /// anzuzeigen. public static func cacheSizeBytes() -> Int64 { guard let hub = effectiveCacheURL() else { return 0 } guard let enumerator = FileManager.default.enumerator( at: hub, includingPropertiesForKeys: [.totalFileAllocatedSizeKey, .isRegularFileKey] ) else { return 0 } var total: Int64 = 0 for case let url as URL in enumerator { let values = try? url.resourceValues(forKeys: [.totalFileAllocatedSizeKey, .isRegularFileKey]) if values?.isRegularFile == true, let size = values?.totalFileAllocatedSize { total += Int64(size) } } return total } /// Löscht ein konkretes Modell-Repo aus dem Shared-Cache. Achtung: /// betrifft alle teilnehmenden Apps. UIs sollten das explizit /// kommunizieren ("Modelle für alle mana-Apps entfernen"). public static func removeModel(repo: String) throws { guard let dir = modelDirURL(repo: repo) else { return } guard FileManager.default.fileExists(atPath: dir.path()) else { return } try FileManager.default.removeItem(at: dir) } }