mana-swift-llm/CHANGELOG.md
till 5a25d44bd0 Release v0.4.0
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 22:42:19 +02:00

186 lines
9 KiB
Markdown

# Changelog
Alle Änderungen werden hier dokumentiert. Format orientiert an
[Keep a Changelog](https://keepachangelog.com), Versionierung nach
[Semver](https://semver.org).
## [Unreleased]
## [0.4.0] — 2026-06-10
### Hinzugefügt — AI-Transparenz-Typ `ManaAiMeta` (ManaLLMShared)
Geteilter Wire-Format-Typ `ManaAiMeta` (Modell · Dienst · Dauer · Tokens ·
Mana · ≈ Strom) für die ökosystemweite KI-Transparenz-Story
(`mana/docs/AI_TRANSPARENCY.md`). Pendant zu `ManaAiMeta` in
`@mana/shared-types`/`@mana/llm-client` (Web). Eine Produkt-API liest die
`X-Mana-Ai-*`-Header von mana-llm und reicht das Objekt an die App; die
geteilte `ManaAiActionMetaView` (mana-swift-ui) rendert es.
- **`ManaAiMeta`** in `ManaLLMShared` — `Codable, Equatable, Sendable`,
public init. Schmale Lib, keine MLX-Dep.
- **Apps:** kein Anpassungsbedarf. Optionaler Adoptionspfad für die
ManaLLM-Facade-Apps (memoro/wordeck/pageta/…).
### Hinzugefügt — BYOK TTS (Phase B, speak)
BYOK kann jetzt **Audio aus Text** erzeugen, symmetrisch zur Web-Seite
(`@mana/byok-providers` `speech.ts`). Anders als Chat/Vision (SSE-Text)
liefert TTS Binär-Audio.
- **`ByokCapability.speak`** + TTS-Metadaten auf `ByokProviderID`
(`speechModels`/`voices`/`defaultSpeechModel`/`defaultVoice`); `speak`
in `supportedCapabilities` **nur** für OpenAI + Gemini.
- **`ByokSpeech.swift`** — `byokSynthesizeSpeech(_:apiKey:model:text:voice:
format:)`-Dispatcher. OpenAI (`/v1/audio/speech`) liefert ein fertiges
Container-Format (Default mp3). Gemini (`:generateContent`,
`responseModalities:["AUDIO"]`) liefert **rohes 16-bit-PCM**
(`audio/L16`) → `byokPCMToWav` verpackt es zu abspielbarem WAV,
`byokParsePCMRate` liest die Sample-Rate aus dem MIME.
- **`ManaLLM.synthesizeSpeech(text:voice:model:provider:format:)`** — Facade-
Verb, greift nur bei BYOK-Wahl + `speak`-fähigem Provider (sonst `nil` →
App nutzt Server/mana-tts). `provider` wird als Wunsch an den Vault-
Resolver durchgereicht (App-Picker).
- 8 Tests (WAV-Header + Samples unangetastet, `parsePCMRate`, speak-
Registry, Facade-Gating inkl. preferred-Provider), netz-/keychain-frei.
`swift test` 47/47 grün.
### Hinzugefügt — Nativer EXIF-Strip für Vision (Compliance)
- **`ByokImagePrep.prepareForVision(_:maxEdge:quality:)`** — Swift-Pendant
zu `prepareImageForVision` (web): re-enkodiert ein Bild über ImageIO,
**strippt alle Metadaten (EXIF inkl. GPS)** und cappt die Kantenlänge
(Default 2048). Re-Encode ohne Metadaten-Dict + `…ThumbnailWithTransform`
(Orientierung in die Pixel) → kein Standort-/Orientation-Tag verlässt
das Gerät. Pflicht vor `describeImage` (COMPLIANCE.md §5).
- **`ManaLLM.describeImage(rawImageData:prompt:…)`** — Convenience, die den
EXIF-Strip selbst macht; so kann ein Native-Konsument ihn nicht
vergessen (analog Web `describeImageStructured`). 3 Tests (Downscale-Cap,
Klein-Bild, Garbage→nil), netzwerk-/fixture-frei.
### Hinzugefügt — BYOK Vision (Phase A, multimodal)
BYOK kann jetzt **Bilder verstehen**, nicht nur Text. Vision läuft bei
allen vier Providern über denselben Chat-Endpoint + denselben Schlüssel
— nur die Nachricht wird multimodal.
- `ByokContentPart` (`.text` / `.imageData(Data, mimeType:)`) +
multimodale `ByokMessage` (`parts`); der `init(role:content:)`-Text-Init
bleibt **back-compat** (alle bestehenden Text-Pfade unverändert).
`textContent` / `hasImages`-Helfer.
- `ByokCapability` (`textChat` / `vision`; `imageGen`/`transcribe`/`speak`
reserviert für Phase B/C) + `ByokProviderID.supportedCapabilities` /
`visionModels` / `defaultVisionModel`. Alle vier können Vision.
- Adapter serialisieren Bild-Parts pro Schema (OpenAI/Mistral
`image_url`-Data-URI, Anthropic `source.base64`, Gemini `inline_data`).
Reine Text-Encodings bleiben byte-gleich.
- `ManaLLM.describeImage(prompt:imageData:mimeType:…)` — Vision-Verb der
Facade. Greift **nur** bei BYOK-Wahl + vision-fähigem Provider; **kein**
lokaler/Server-Fallback (On-Device-Vision experimentell) → `nil` ⇒ App
rendert ihren Server-/metered-Pfad. `ByokFacadeBox.selectedVision`
substituiert das Vision-Modell des Providers.
- 9 neue, netzwerk-freie Tests (Encoding-Form + Capability-Registry +
Vision-Gating). `swift test` 23/23 grün.
> **Datenschutz:** Bilder sind sensibler als Text — Apps strippen EXIF +
> cappen die Kantenlänge vor dem Vision-Call (COMPLIANCE.md §5).
### Hinzugefügt — BYOK in der `ManaLLM`-Facade
- **`ManaLLM.configureByok(vault:defaultProvider:)`** — optionaler Boot-
Call (neben `configure()`). Danach routen `generate`/`summarize`/
`classify` **direkt zum Dritt-Anbieter**, sobald der Nutzer in den
Settings „Eigener Schlüssel (BYOK)" wählt
(`LLMBackendPreferenceStore.current == .byok`) **und** ein Schlüssel im
Vault liegt. Ohne den Call bleibt BYOK inert; ohne hinterlegten
Schlüssel fällt die Facade still auf die lokale Backend-Priorität
zurück. Der Default-`ByokKeyVault()` teilt die Keychain-Einträge mit der
Settings-UI (`ManaLLMSettingsState` in mana-swift-ui).
- Intern: `ByokFacadeBox` (NSLock-geschützter, `@unchecked Sendable`
Resolver-Halter; `selectedBackend(currentBackend:)` ist rein/testbar).
4 neue Tests in `ByokTests`.
> **App-Adoption:** ein Einzeiler `ManaLLM.configureByok()` im App-Boot
> (Facade-Apps: pageta / herbatrium / comicello). Apps mit eigenem
> `LLMRouter` (memoro) verdrahten `setByokResolver(...)` direkt.
## [0.3.0] — 2026-06-04
### Hinzugefügt — BYOK (Bring Your Own Key)
Neue Quelle `LLMBackendID.byok`: der Nutzer hinterlegt einen **eigenen**
Dritt-API-Key (OpenAI / Anthropic / Gemini / Mistral); der Call geht
**direkt vom Gerät** zum Anbieter — der Key berührt mana-Infrastruktur
nie. Symmetrisch zum Web-Pendant `@mana/browser-llm` + `@mana/byok-providers`.
- `Sources/ManaLLM/Byok/`:
- `ByokTypes.swift` — `ByokProviderID`, `ByokSelection`, `ByokMessage`,
`ByokKeyResolver`, `ByokProvider`-Protocol, `ByokError`.
- `ByokProviders.swift` — vier `URLSession`-Streaming-Adapter (SSE),
OpenAI/Mistral via gemeinsamen OpenAI-compat-Pfad, Anthropic + Gemini
mit ihren eigenen Schemas.
- `ByokKeyVault.swift` — Keychain-Speicher, **bewusst app-privat, NICHT**
in `group.ev.mana.session`; keine Cross-Device-Sync. `makeResolver(…)`.
- `ByokBackend.swift` — `LLMBackend`-Konformität; ohne Resolver
`unavailable`; Prompt/Key werden nie geloggt.
- `LLMRouter`: `setByokResolver(_:defaultProvider:)`, `setAllowByokInPick(_:)`.
BYOK wird **nie still gewählt** (Datenschutz-Disziplin, analog Web) —
direkt nutzbar über `backend(for: .byok)`.
- `LLMBackendID.byok.isOnDeviceLLM == false` (Inhalt geht zu Dritt-SaaS).
**Apps müssen anpassen:** `LLMBackendID` hat einen neuen Case `.byok` —
`switch` über alle Cases (z.B. in Settings-UI) muss ihn behandeln.
Compliance-Regel (sensible Inhalte per Default nicht über BYOK) siehe
`mana/docs/COMPLIANCE.md §5`. 11 neue Tests (netzwerk-/keychain-frei),
`swift build` + `swift test` grün (23 gesamt).
## [0.2.0] — 2026-05-22
Minor — **Preference-Stores nach `ManaLLM` geliftet** + UI-Konsoldierung
in `mana-swift-ui` möglich gemacht.
### Hintergrund
Vorher lebten `LLMBackendPreferenceStore` und
`LLMDownloadOverCellularStore` mit Memoro-spezifischen UserDefaults-Keys
(`memoro.llmBackend`, `memoro.llmDownloadOverCellular`) ausschließlich
in `memoro-native`. Damit die anderen ManaLLM-Konsumenten (Pageta,
Comicello, Herbatrium) ebenfalls eine Settings-UI bekommen können
(siehe `mana-swift-ui` 0.8.0 / `ManaLLMUI`), wandern die Stores hier
ins Package und nutzen App-übergreifende Keys (`mana.llm.*`).
### Neu
- `LLMBackendPreferenceStore` — UserDefaults-Store für die On-Device-
LLM-Wahl (`mana.llm.backend`). Default: `.appleFM`.
- `LLMDownloadOverCellularStore` — WiFi-only-Default für Modell-
Downloads (`mana.llm.allowCellular`). Default: `false`.
- Beide Stores migrieren beim ersten Read einmalig aus Legacy-Keys:
- `memoro.llmBackend` → `mana.llm.backend`
- `memoro.onDeviceLLMEnabled` (alter Bool-Toggle) → mapped zu
`.appleFM` / `.noOp`, dann `mana.llm.backend`
- `memoro.llmDownloadOverCellular` → `mana.llm.allowCellular`
- `LLMBackend.removeCachedModel()` — neue Protocol-Methode mit
Default-No-Op-Impl. `GemmaBackend` überschreibt (löscht
HF-Repo-Pfad im App-Group-Container). Apple FM und NoOp bleiben
No-Op.
- Test-Suite `LLMPreferenceStoresTests` (13 Tests, `.serialized`
wegen `UserDefaults.standard`).
### Geändert
- `GemmaBackend.removeCachedModel()`-Signatur: `throws` → `async throws`
(für Protocol-Konformität). Caller müssen `try await` schreiben statt
`try`.
### Migration für Apps
Apps die schon `ManaLLM` konsumieren brauchen nichts zu ändern —
Symbol-Namen bleiben identisch, Defaults bleiben identisch, alte
UserDefaults-Keys werden transparent migriert. Memoro hat seine
lokalen Store-Duplikate gelöscht; der Build bleibt grün, weil
Memoro `import ManaLLM` schon hatte.
Apps die heute keine LLM-Settings-UI haben können jetzt
`ManaLLMUI` aus `mana-swift-ui` 0.8.0 einhängen (drop-in).