Neues Swift-Package mit lokalen LLM-Backends für alle nativen mana- e.V.-Apps. Lift der bisher Memoro-eigenen Files in `memoro-native/Sources/Core/AI/` plus zwei neue Layer: ManaSharedModels (App-Group-Container-Helper) und ManaLLM-Facade. Library-Products: - ManaLLM — Backend-Abstraktion (FoundationModels, Gemma 4 E2B/E4B, NoOp), Router mit Priority-Liste, High-Level-Facade `ManaLLM.summarize/generate/classify` mit fast/creative/deep Level. - ManaLLMShared — App-Group `group.ev.mana.models` Container, HF_HUB_CACHE-Setup, Legacy-Fallback wenn Group fehlt. Lift-Anpassungen ggü. memoro: - public-Marker auf protocol + types + actors - generischer `generate(prompt:instructions:maxTokens:)` zu LLMBackend-Protocol hinzu; `summarize` als Default-Impl auf Basis von generate - AppleFMBackend behält optimierten @Generable-Summary-Path - GemmaBackend nutzt ManaSharedModels.effectiveCacheURL() statt eigenen Application-Support-Pfad; allowsCellular kommt jetzt als Initializer-Param statt App-Settings-Lookup - LLMRouter: Memoro-spezifische User-Pref-Store-Logic durch Priority-Liste-API ersetzt - LLMLog-Subsystem `ev.mana.llm` statt App-eigenes `Log.ai` Build: `swift build` clean (76s, MLX-Toolchain-Resolution beim ersten Lauf). 4/4 Parser-Tests grün. Doku: ../mana/docs/MANA_LLM.md (Plattform-SOT), CLAUDE.md (Konventionen + Lift-Tabelle). Folge: L-4 Memoro auf ManaLLM umstellen, L-5 pageta-Pilot. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
148 lines
5.3 KiB
Swift
148 lines
5.3 KiB
Swift
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.<app>
|
|
/// - 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:
|
|
/// `<hub>/models--<owner>--<name>`.
|
|
///
|
|
/// Beispiel:
|
|
/// ```swift
|
|
/// ManaSharedModels.modelDirURL(repo: "mlx-community/gemma-4-e2b-it-4bit")
|
|
/// // → <hub>/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)
|
|
}
|
|
}
|