diff --git a/deployment/on-device/leap-sdk-changelog.mdx b/deployment/on-device/leap-sdk-changelog.mdx index 0c1a4d7..7fc0ffe 100644 --- a/deployment/on-device/leap-sdk-changelog.mdx +++ b/deployment/on-device/leap-sdk-changelog.mdx @@ -3,7 +3,7 @@ title: "Changelog" description: "Release notes for the LEAP SDK, including the 0.9.x → 0.10.x Kotlin Multiplatform transition." --- -Latest release: **v0.10.7** ([GitHub](https://github.com/Liquid4All/leap-sdk/releases/tag/v0.10.7)). +Latest release: **v0.10.9** ([GitHub](https://github.com/Liquid4All/leap-sdk/releases/tag/v0.10.9)). This page covers user-visible changes in the LEAP SDK across releases. For per-build commit detail, see the release notes on [`Liquid4All/leap-sdk`](https://github.com/Liquid4All/leap-sdk/releases). @@ -147,6 +147,77 @@ val runner = downloader.loadModel( ## Per-release notes +### v0.10.9 — 2026-05-29 + +Fixes a Swift-only hang when constructing the model downloader on Apple targets. No other API changes; binary-compatible with v0.10.8 apart from the Swift `LeapDownloaderConfig` construction shape. + +**`LeapDownloaderConfig` / `ModelDownloader` Swift construction no longer hangs** ([PR #262](https://github.com/Liquid4All/leap-android-sdk/pull/262)): + +- In v0.10.6–v0.10.8, constructing `LeapDownloaderConfig(...)`, `LeapDownloader(...)`, or `ModelDownloader()` from Swift spun the calling thread at 100% CPU **forever** and never returned — blocking the `loadSimpleModel(...)` sideload path. Every field of the Kotlin `LeapDownloaderConfig` `data class` bridges to a native Swift type (`String`, `Bool`, `String?`, `Int64`), so the v0.10.6 defaulted `convenience init` had the exact same Swift selector as the Kotlin/Native ObjC designated initializer it delegated to. A Swift convenience init shadows a same-selector imported designated init, so `self.init(...)` resolved back to itself — an infinite recursion that Release builds optimize into a busy loop. +- The defaulted convenience init is replaced by a **`LeapDownloaderConfig.with(...)` static factory** carrying the same Kotlin defaults (`saveDir = "leap_models"`, `validateSha256 = true`, `disableSslValidation = false`, `baseUrl = nil`, `connectTimeoutMillis = 30_000`, `socketTimeoutMillis = 60_000`, `requestTimeoutMillis = 600_000`). A `static func` has no `self.init` relationship, so its body binds to the designated initializer with no recursion. + +**Swift call-site change:** + +```diff +- let config = LeapDownloaderConfig(saveDir: modelsDir) ++ let config = LeapDownloaderConfig.with(saveDir: modelsDir) +``` + +- `ModelDownloader()` / `ModelDownloader(sessionConfiguration:)` / `ModelDownloader(config:)` are unchanged at the call site — they build their default config through the factory internally. +- The full seven-argument designated initializer `LeapDownloaderConfig(saveDir:validateSha256:disableSslValidation:baseUrl:connectTimeoutMillis:socketTimeoutMillis:requestTimeoutMillis:)` also works directly if you prefer passing every field. +- **Android / JVM / Kotlin-Native** Kotlin callers are unaffected — the Kotlin `LeapDownloaderConfig(saveDir = ...)` data-class constructor never had this issue. + +See [Model Loading](/deployment/on-device/sdk/model-loading) for the updated Swift call sites. + +### v0.10.8 — 2026-05-21 + +SKIE finally lands on `leap-sdk-openai-client`, closing the last Apple-side gap in the unified KMP surface. In the same release, generation errors gain an in-band representation so they survive the SKIE `Flow` → `AsyncSequence` bridge. + +**SKIE applied to `leap-sdk-openai-client`** ([PR #258](https://github.com/Liquid4All/leap-android-sdk/pull/258)): + +- `client.streamChatCompletion(request:)` is now a Swift `AsyncSequence` — collect with `for try await event in client.streamChatCompletion(...)` instead of the v0.10.6 / v0.10.7 manual `Flow.collect(collector: FlowCollector { ... })` shape. +- `onEnum(of: event)` switches exhaustively over `ChatCompletionEvent` — the compiler errors on a missing case. +- Nested Kotlin class names are preserved on the Swift side: `ChatCompletionEvent.Delta`, `ChatCompletionEvent.Done`, `ChatCompletionEvent.Error` (previously K/N flattened these to `ChatCompletionEventDelta`, etc.). `ChatMessage.System` / `ChatMessage.User` / `ChatMessage.Assistant` follow the same convention now. +- The top-level Kotlin `fun OpenAiClient(config:)` factory exports as a real Swift convenience init — Swift call sites can drop the `OpenAiClientKt.` prefix and just write `OpenAiClient(config: ...)`. +- `LeapOpenAIClient.xcframework` is now a distributable framework (`skie { build { produceDistributableFramework() } }`) that bundles the SKIE-generated Swift wrappers, so a plain `import LeapOpenAIClient` is enough — no companion SKIE setup in consumer projects. +- The Kotlin / JVM / wasmJs / K/N surface is unchanged — same `OpenAiClient(config:)` constructor, same `Flow`, same `ChatCompletionEvent.Delta` / `.Done` / `.Error` cases, same `OpenAiClientConfig` shape and OpenRouter extra-headers support. + +See [OpenAI-Compatible Client](/deployment/on-device/sdk/openai-client) for the updated Swift call sites. + +**In-band `MessageResponse.Error` (Kotlin) / `LiquidMessageResponse.Error` (Swift compat layer)** ([PR #258](https://github.com/Liquid4All/leap-android-sdk/pull/258)): + +`Conversation.generateResponse(...)` now emits a terminal `MessageResponse.Error(throwable, message)` value **before** closing the underlying channel with the same throwable. This closes a Swift gap: SKIE bridges Kotlin `Flow` to a Swift `AsyncSequence` that does not rethrow Kotlin exceptions, so a thrown `LeapGenerationException` could not be surfaced to a Swift consumer iterating the stream. With the new in-band case, Swift consumers `switch onEnum(of: response)` over the `.error` arm and surface the failure to the user; Kotlin consumers using `Flow.catch { }` / `try/catch` keep working unchanged because the exceptional close is still emitted. + +```kotlin +sealed interface MessageResponse { + // ... existing cases ... + data class Error( + val throwable: Throwable, + val message: String = throwable.message ?: throwable::class.simpleName ?: throwable.toString(), + ) : MessageResponse +} +``` + +**Public API: additive** — binary-compatible — but exhaustive consumers need a small fix: + +- **Kotlin:** add `is MessageResponse.Error -> { ... }` to any exhaustive `when (response)` over `MessageResponse`, or an `else -> { }` fallback. Statement-position `when` blocks compile with a warning today; expression-position `when` blocks (returning a value) won't compile until the new arm is added. +- **Swift:** add `case .error(let err): ...` to any `switch onEnum(of: response)` or `switch onEnum(of: liquidResponse)` — the Swift compiler enforces exhaustiveness, so call sites stop compiling against v0.10.8 until updated. + +The same shape mirrors to the iOS compat layer as `LiquidMessageResponse.Error(throwable, message)` for callers still on the 0.9.x `LiquidConversation` API surface. + + +**Terminal-operator caveat (Kotlin).** Terminal operators that stop after the first matching event — `first()`, `first { ... }`, `take(1)` — may now consume the in-band `MessageResponse.Error` value and complete *normally*, where previously they would have propagated the underlying exception. Pattern-match on the result before acting (or keep a `.catch { }` operator wired up) if your retry pipeline depended on exceptional completion. + + +The `Flow` returned by `generateResponse` is now backed by an unlimited-capacity buffer (`buffer(Channel.UNLIMITED)`) so the terminal `Error` emission can never be dropped under collector backpressure — losing it would silently strand Swift callers. Generated token volume is bounded by the model's max-tokens setting, so the memory cost is negligible. + +**Toolchain bumps** (no consumer-visible effect): + +- Kotlin 2.3.20 → **2.3.21**. +- SKIE 0.10.11 → **0.10.12**. + +iOS / macOS Swift consumers don't need to bump Xcode or Swift — the Xcode 16 / Swift 6 baseline from v0.10.0 still holds. + ### v0.10.7 — 2026-05-18 KMP target completion for `leap-openai-client` plus a repo-wide bytecode-hardening pass. iOS / macOS Swift surface is unchanged from v0.10.6 — this is a Kotlin/JVM ergonomics release for non-Apple consumers. @@ -156,7 +227,7 @@ KMP target completion for `leap-openai-client` plus a repo-wide bytecode-hardeni - **`jvm`** (Ktor CIO engine) — Maven Central now publishes `ai.liquid.leap:leap-openai-client-jvm:0.10.7`. Pure-JVM desktop / server apps can route OpenAI-compatible chat completions without dragging in Android or KMP targets. (The 0.10.0 — 0.10.6 SPM cascade only shipped Android + Apple + Linux/MinGW K/N + wasmJs metadata; the JVM slice was absent.) - **`wasmJs`** (Ktor Js engine) — browser-side chat-completions client matching what `leap-sdk` already targets. -The Apple slice (`LeapOpenAIClient.xcframework`) ships unchanged — same SSE-stream surface, same `OpenAiClientConfig`, same OpenRouter extra-headers support. SKIE is still not applied to this module in v0.10.7, so the Kotlin/Native exports remain the same as v0.10.6: `Flow` is not bridged to Swift `AsyncSequence`, and `onEnum(of:)` is not generated for `ChatCompletionEvent`. **The next release will enable SKIE on `leap-sdk-openai-client`**, bringing `for try await` over the stream, exhaustive `onEnum(of:)` switching, and SKIE-bundled Swift convenience inits — see the [OpenAI client page](/deployment/on-device/sdk/openai-client) for the current pinning guidance. +The Apple slice (`LeapOpenAIClient.xcframework`) ships unchanged — same SSE-stream surface, same `OpenAiClientConfig`, same OpenRouter extra-headers support. SKIE is still not applied to this module in v0.10.7, so the Kotlin/Native exports remain the same as v0.10.6: `Flow` is not bridged to Swift `AsyncSequence`, and `onEnum(of:)` is not generated for `ChatCompletionEvent`. **[v0.10.8](#v0-10-8-2026-05-21) enables SKIE on `leap-sdk-openai-client`** — see that entry for the new Swift surface (`for try await` over the stream, exhaustive `onEnum(of:)` switching, SKIE-bundled `OpenAiClient(config:)` convenience init). Pin to v0.10.7 only if you need the pre-SKIE surface frozen. **Bytecode hardening:** diff --git a/deployment/on-device/sdk/ai-agent-usage-guide.mdx b/deployment/on-device/sdk/ai-agent-usage-guide.mdx index bd117c2..2015193 100644 --- a/deployment/on-device/sdk/ai-agent-usage-guide.mdx +++ b/deployment/on-device/sdk/ai-agent-usage-guide.mdx @@ -58,6 +58,8 @@ Every agent has the same shape: send a `ChatMessage`, iterate the response strea if let stats = completion.stats { log("Done: \(stats.totalTokens) tokens at \(stats.tokenPerSecond) tok/s") } + case .error(let err): + log("Generation failed: \(err.message)") } } ``` @@ -76,6 +78,7 @@ Every agent has the same shape: send a `ChatMessage`, iterate the response strea _text.value = "" Log.d(TAG, "Done: ${response.stats?.totalTokens} tokens at ${response.stats?.tokenPerSecond} tok/s") } + is MessageResponse.Error -> Log.e(TAG, "Generation failed: ${response.message}", response.throwable) } } ``` @@ -103,8 +106,10 @@ The defining feature of an agent: the model emits `FunctionCalls`, you execute t toolCalls.append(contentsOf: payload.functionCalls) case .complete: break - default: + case .reasoningChunk, .audioSample: break + case .error(let err): + throw NSError(domain: "AgentLoop", code: 1, userInfo: [NSLocalizedDescriptionKey: err.message]) } } @@ -139,7 +144,10 @@ The defining feature of an agent: the model emits `FunctionCalls`, you execute t when (response) { is MessageResponse.Chunk -> appendUI(response.text) is MessageResponse.FunctionCalls -> toolCalls.addAll(response.functionCalls) - else -> {} + is MessageResponse.ReasoningChunk -> {} + is MessageResponse.AudioSample -> {} + is MessageResponse.Complete -> {} + is MessageResponse.Error -> throw response.throwable } } @@ -252,7 +260,7 @@ A `ChatViewModel` that loads the model, registers a tool, drives generation, and private let downloader: ModelDownloader = { let caches = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first!.path let modelsDir = (caches as NSString).appendingPathComponent("leap_models") - return ModelDownloader(config: LeapDownloaderConfig(saveDir: modelsDir)) + return ModelDownloader(config: LeapDownloaderConfig.with(saveDir: modelsDir)) }() private var modelRunner: ModelRunner? private var conversation: Conversation? @@ -317,6 +325,8 @@ A `ChatViewModel` that loads the model, registers a tool, drives generation, and if let stats = c.stats { print("\nFinished — \(stats.totalTokens) tokens at \(stats.tokenPerSecond) tok/s") } + case .error(let err): + errorMessage = "Generation failed: \(err.message)" } } } @@ -408,6 +418,7 @@ A `ChatViewModel` that loads the model, registers a tool, drives generation, and is MessageResponse.FunctionCalls -> response.functionCalls.forEach { dispatch(it) } is MessageResponse.AudioSample -> audioRenderer.enqueue(response.samples, response.sampleRate) is MessageResponse.Complete -> Log.d(TAG, "Done: ${response.stats?.totalTokens} tokens") + is MessageResponse.Error -> _errorMessage.value = "Generation failed: ${response.message}" } } diff --git a/deployment/on-device/sdk/cloud-ai-comparison.mdx b/deployment/on-device/sdk/cloud-ai-comparison.mdx index 06be32e..cbe0b69 100644 --- a/deployment/on-device/sdk/cloud-ai-comparison.mdx +++ b/deployment/on-device/sdk/cloud-ai-comparison.mdx @@ -43,7 +43,7 @@ Cloud APIs create a thin client that points at a remote endpoint. LEAP downloads let caches = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first!.path let modelsDir = (caches as NSString).appendingPathComponent("leap_models") - let downloader = ModelDownloader(config: LeapDownloaderConfig(saveDir: modelsDir)) + let downloader = ModelDownloader(config: LeapDownloaderConfig.with(saveDir: modelsDir)) let runner = try await downloader.loadModel( modelName: "LFM2.5-1.2B-Instruct", @@ -127,6 +127,8 @@ Cloud APIs deliver deltas; you concatenate them. LEAP delivers `MessageResponse` print("\nDone! Tokens: \(completion.stats?.totalTokens ?? 0)") case .reasoningChunk, .audioSample, .functionCalls: break + case .error(let err): + print("\nGeneration failed: \(err.message)") } } ``` @@ -137,7 +139,10 @@ Cloud APIs deliver deltas; you concatenate them. LEAP delivers `MessageResponse` when (response) { is MessageResponse.Chunk -> print(response.text) is MessageResponse.Complete -> println("\nDone! Tokens: ${response.stats?.totalTokens}") - else -> {} + is MessageResponse.ReasoningChunk -> {} + is MessageResponse.FunctionCalls -> {} + is MessageResponse.AudioSample -> {} + is MessageResponse.Error -> println("\nGeneration failed: ${response.message}") } }.collect() ``` @@ -161,7 +166,7 @@ Both LEAP and the OpenAI Python streaming client run inside an async context. Th private let downloader: ModelDownloader = { let caches = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first!.path let modelsDir = (caches as NSString).appendingPathComponent("leap_models") - return ModelDownloader(config: LeapDownloaderConfig(saveDir: modelsDir)) + return ModelDownloader(config: LeapDownloaderConfig.with(saveDir: modelsDir)) }() func loadModel() async { diff --git a/deployment/on-device/sdk/conversation-generation.mdx b/deployment/on-device/sdk/conversation-generation.mdx index e9815a2..298a8cc 100644 --- a/deployment/on-device/sdk/conversation-generation.mdx +++ b/deployment/on-device/sdk/conversation-generation.mdx @@ -168,6 +168,11 @@ The async stream is the recommended way to drive generation — both platforms e if let stats = completion.stats { print("Prompt tokens: \(stats.promptTokens), completion: \(stats.completionTokens)") } + case .error(let err): + // Generation failures surface as this in-band `.error` case (added in + // v0.10.8). The SKIE-bridged stream does not rethrow the Kotlin exception + // to Swift, so handle the failure here and present `err.message` to the user. + print("\nGeneration failed:", err.message) } } } catch { @@ -199,9 +204,12 @@ The async stream is the recommended way to drive generation — both platforms e is MessageResponse.FunctionCalls -> handleFunctionCalls(response.functionCalls) is MessageResponse.AudioSample -> audioRenderer.enqueue(response.samples, response.sampleRate) is MessageResponse.Complete -> Log.d(TAG, "Done. Stats: ${response.stats}") + is MessageResponse.Error -> Log.e(TAG, "Generation failed: ${response.message}", response.throwable) } } - ?.catch { e -> Log.e(TAG, "Generation failed", e) } + // The flow also closes with the same throwable after the in-band + // `Error` above; swallow it here so the failure isn't logged twice. + ?.catch { } ?.collect() } } @@ -216,10 +224,14 @@ The async stream is the recommended way to drive generation — both platforms e } ``` - Errors propagate as `LeapGenerationException` through the flow — handle with `.catch { ... }`. + Generation errors arrive **two ways** since v0.10.8: as a terminal `MessageResponse.Error(throwable, message)` value emitted before the flow closes (so Swift consumers, whose SKIE-bridged `AsyncSequence` does not rethrow the Kotlin exception, can react in-band), **and** as a `LeapGenerationException` thrown when the flow closes (so Kotlin's `Flow.catch { ... }` and `try`/`catch` keep working). Pick one path or the other — they describe the same failure. + +**Terminal-operator caveat (Kotlin, v0.10.8+).** Terminal operators that stop after the first matching event — `first()`, `first { ... }`, `take(1)` — may now consume the in-band `MessageResponse.Error` value and complete *normally*, where previously they would have propagated the underlying `LeapGenerationException` exceptionally. Pattern-match on the result before acting (or keep a `.catch { }` operator wired up) if your retry pipeline depended on exceptional completion. + + **Cancellation.** Cancelling the Swift `Task` or the Kotlin coroutine `Job` stops generation and frees native resources. On both platforms cancellation is cooperative — the engine checks between tokens, so there's at most one extra token of slack after `cancel()`. @@ -259,6 +271,7 @@ A sealed type with one case per kind of incremental output the engine emits. case functionCalls(FunctionCalls) // FunctionCalls.functionCalls — [LeapFunctionCall] case audioSample(AudioSample) // AudioSample.samples, .sampleRate — PCM frames case complete(Complete) // Complete.fullMessage, .finishReason, .stats + case error(Error) // Error.throwable, .message — generation failure (v0.10.8+) } ``` @@ -276,6 +289,11 @@ A sealed type with one case per kind of incremental output the engine emits. val finishReason: GenerationFinishReason, val stats: GenerationStats?, ) : MessageResponse + // v0.10.8+ + data class Error( + val throwable: Throwable, + val message: String = throwable.message ?: throwable::class.simpleName ?: throwable.toString(), + ) : MessageResponse } ``` @@ -285,7 +303,8 @@ A sealed type with one case per kind of incremental output the engine emits. - **`ReasoningChunk`** — thinking-style tokens emitted by reasoning models (wrapped between `` / `` upstream). Only fires when `GenerationOptions.enableThinking = true` *and* the model supports it. - **`FunctionCalls`** — one or more tool invocations the model wants you to execute. See [Function Calling](./function-calling). - **`AudioSample`** — float32 mono PCM frames from audio-capable checkpoints. The sample rate is constant for a generation; route the frames to a renderer. -- **`Complete`** — final marker. `fullMessage` is the assembled assistant `ChatMessage` (also present in `conversation.history`). `stats` is nullable (`GenerationStats?`); when present it holds `promptTokens`, `completionTokens`, `totalTokens`, `tokenPerSecond` (non-nullable `Float`), and `cachedPromptTokens`. +- **`Complete`** — final marker for a successful generation. `fullMessage` is the assembled assistant `ChatMessage` (also present in `conversation.history`). `stats` is nullable (`GenerationStats?`); when present it holds `promptTokens`, `completionTokens`, `totalTokens`, `tokenPerSecond` (non-nullable `Float`), and `cachedPromptTokens`. +- **`Error`** _(v0.10.8+)_ — terminal failure event. `throwable` is the underlying cause (typically a `LeapGenerationException` or one of its subclasses); `message` defaults to `throwable.message`, falling back to the throwable's simple class name. Emitted as the final event before the flow closes — Kotlin consumers can additionally rely on `Flow.catch { }` / `try`/`catch`, but Swift consumers (whose SKIE-bridged `AsyncSequence` does not rethrow the Kotlin exception) must handle this case to detect generation failures. Partial output that arrived before the error is **not** appended to `conversation.history`. The data-class `equals` / `hashCode` falls back to reference equality on `throwable` — see the per-field comparison note in the SDK Kotlin docs if you need value-style equality. ### `GenerationFinishReason` diff --git a/deployment/on-device/sdk/desktop-platforms.mdx b/deployment/on-device/sdk/desktop-platforms.mdx index 26ca1c6..40c4315 100644 --- a/deployment/on-device/sdk/desktop-platforms.mdx +++ b/deployment/on-device/sdk/desktop-platforms.mdx @@ -40,7 +40,7 @@ The JVM target supports Kotlin and Java projects on macOS (Apple Silicon), Linux ```kotlin plugins { - kotlin("jvm") version "2.3.20" + kotlin("jvm") version "2.3.21" application } @@ -49,13 +49,13 @@ The JVM target supports Kotlin and Java projects on macOS (Apple Silicon), Linux } dependencies { - implementation("ai.liquid.leap:leap-sdk:0.10.7") + implementation("ai.liquid.leap:leap-sdk:0.10.9") // Optional: OpenAI-compatible cloud chat client (JVM support added in v0.10.7) - // implementation("ai.liquid.leap:leap-openai-client:0.10.7") + // implementation("ai.liquid.leap:leap-openai-client:0.10.9") // Optional: Compose Multiplatform voice widget (also runs on JVM) - // implementation("ai.liquid.leap:leap-ui:0.10.7") + // implementation("ai.liquid.leap:leap-ui:0.10.9") } application { @@ -66,7 +66,7 @@ The JVM target supports Kotlin and Java projects on macOS (Apple Silicon), Linux ```groovy plugins { - id 'org.jetbrains.kotlin.jvm' version '2.3.20' + id 'org.jetbrains.kotlin.jvm' version '2.3.21' id 'application' } @@ -75,7 +75,7 @@ The JVM target supports Kotlin and Java projects on macOS (Apple Silicon), Linux } dependencies { - implementation 'ai.liquid.leap:leap-sdk:0.10.7' + implementation 'ai.liquid.leap:leap-sdk:0.10.9' } application { @@ -89,7 +89,7 @@ The JVM target supports Kotlin and Java projects on macOS (Apple Silicon), Linux ai.liquid.leap leap-sdk-jvm - 0.10.7 + 0.10.9 ``` @@ -137,7 +137,10 @@ fun main() = runBlocking { when (response) { is MessageResponse.Chunk -> print(response.text) is MessageResponse.Complete -> println("\n[${response.stats?.totalTokens} tokens]") - else -> {} + is MessageResponse.ReasoningChunk -> {} + is MessageResponse.FunctionCalls -> {} + is MessageResponse.AudioSample -> {} + is MessageResponse.Error -> System.err.println("\n[error] ${response.message}") } } @@ -193,12 +196,12 @@ dependencyResolutionManagement { ```kotlin // build.gradle.kts plugins { - kotlin("multiplatform") version "2.3.20" - id("ai.liquid.leap.nativelibs") version "0.10.7" + kotlin("multiplatform") version "2.3.21" + id("ai.liquid.leap.nativelibs") version "0.10.9" } dependencies { - implementation("ai.liquid.leap:leap-sdk:0.10.7") + implementation("ai.liquid.leap:leap-sdk:0.10.9") } kotlin { @@ -223,11 +226,11 @@ The resulting binary lives at `build/bin/linuxX64/releaseExecutable/`, alongside ```kotlin plugins { - kotlin("multiplatform") version "2.3.20" + kotlin("multiplatform") version "2.3.21" } dependencies { - implementation("ai.liquid.leap:leap-sdk:0.10.7") + implementation("ai.liquid.leap:leap-sdk:0.10.9") } val nativesDir = layout.buildDirectory.dir("bin/linuxX64/releaseExecutable") @@ -241,7 +244,7 @@ kotlin { val leapSdkNatives by configurations.creating dependencies { - leapSdkNatives("ai.liquid.leap:leap-sdk-linuxx64:0.10.7:natives@zip") + leapSdkNatives("ai.liquid.leap:leap-sdk-linuxx64:0.10.9:natives@zip") } val installLeapNatives by tasks.registering(Copy::class) { @@ -259,8 +262,8 @@ tasks.named("linkReleaseExecutableLinuxX64") { dependsOn(installLeapNatives) } The Maven coordinates for the `-natives.zip` artifacts: -- `ai.liquid.leap:leap-sdk-linuxx64:0.10.7:natives@zip` -- `ai.liquid.leap:leap-sdk-linuxarm64:0.10.7:natives@zip` +- `ai.liquid.leap:leap-sdk-linuxx64:0.10.9:natives@zip` +- `ai.liquid.leap:leap-sdk-linuxarm64:0.10.9:natives@zip` ## Windows native (MinGW x64) @@ -268,12 +271,12 @@ The same Kotlin/Native flow works for Windows x86_64 via the MinGW-w64 toolchain ```kotlin plugins { - kotlin("multiplatform") version "2.3.20" - id("ai.liquid.leap.nativelibs") version "0.10.7" + kotlin("multiplatform") version "2.3.21" + id("ai.liquid.leap.nativelibs") version "0.10.9" } dependencies { - implementation("ai.liquid.leap:leap-sdk:0.10.7") + implementation("ai.liquid.leap:leap-sdk:0.10.9") } kotlin { @@ -291,10 +294,10 @@ The plugin installs `inference_engine.dll`, `libinference_engine_llamacpp_backen The Maven coordinates for the `-natives.zip` artifact: -- `ai.liquid.leap:leap-sdk-mingwx64:0.10.7:natives@zip` +- `ai.liquid.leap:leap-sdk-mingwx64:0.10.9:natives@zip` -**Building from macOS or Linux for Windows?** Kotlin/Native does not support cross-compiling to MinGW from a non-Windows host as of 2.3.20 — the build must run on Windows (native or in CI). GitHub Actions `windows-latest` works without extra setup. +**Building from macOS or Linux for Windows?** Kotlin/Native does not support cross-compiling to MinGW from a non-Windows host as of 2.3.21 — the build must run on Windows (native or in CI). GitHub Actions `windows-latest` works without extra setup. ## macOS (Apple Silicon) @@ -316,8 +319,8 @@ Identical Swift API to iOS — same `ModelDownloader`, `Conversation`, `ChatMess ```swift .binaryTarget( name: "LeapSDK", - url: "https://github.com/Liquid4All/leap-sdk/releases/download/v0.10.7/LeapSDK.xcframework.zip", - checksum: "6f2721aa45d7555646f78cbcaedb57aba3d869f56b24d681ad332846e131ae3d" + url: "https://github.com/Liquid4All/leap-sdk/releases/download/v0.10.9/LeapSDK.xcframework.zip", + checksum: "1fdea67fe208ee56db3aba4313809b03a469e6520a49a855c212ebcfef4cea2e" ) ``` @@ -329,8 +332,8 @@ If you're targeting macOS as a JVM host — for example with Compose Multiplatfo ```kotlin dependencies { - implementation("ai.liquid.leap:leap-sdk:0.10.7") - implementation("ai.liquid.leap:leap-ui:0.10.7") // Compose voice widget runs on JVM too + implementation("ai.liquid.leap:leap-sdk:0.10.9") + implementation("ai.liquid.leap:leap-ui:0.10.9") // Compose voice widget runs on JVM too } ``` diff --git a/deployment/on-device/sdk/function-calling.mdx b/deployment/on-device/sdk/function-calling.mdx index 1bec418..b249949 100644 --- a/deployment/on-device/sdk/function-calling.mdx +++ b/deployment/on-device/sdk/function-calling.mdx @@ -108,6 +108,8 @@ Function calls arrive as the `MessageResponse.FunctionCalls` variant on both pla } case .chunk, .reasoningChunk, .audioSample, .complete: break + case .error(let err): + print("Generation failed: \(err.message)") } } ``` @@ -134,7 +136,9 @@ Function calls arrive as the `MessageResponse.FunctionCalls` variant on both pla // Tool calls are also surfaced on the assembled assistant message: response.fullMessage.functionCalls?.forEach { /* ... */ } } - else -> {} + is MessageResponse.ReasoningChunk -> {} + is MessageResponse.AudioSample -> {} + is MessageResponse.Error -> Log.e(TAG, "Generation failed: ${response.message}", response.throwable) } }.collect() ``` diff --git a/deployment/on-device/sdk/model-loading.mdx b/deployment/on-device/sdk/model-loading.mdx index e33ee29..858a8dd 100644 --- a/deployment/on-device/sdk/model-loading.mdx +++ b/deployment/on-device/sdk/model-loading.mdx @@ -58,7 +58,7 @@ All downloader classes return the same `ModelRunner` type. They share an on-disk ```swift let downloader = LeapDownloader( - config: LeapDownloaderConfig(saveDir: modelsDir, validateSha256: true) + config: LeapDownloaderConfig.with(saveDir: modelsDir, validateSha256: true) ) ``` @@ -124,7 +124,7 @@ Resolves the GGUF manifest for the given model + quantization slug, downloads an ```swift let downloader = ModelDownloader( - config: LeapDownloaderConfig(saveDir: modelsDir, validateSha256: true) + config: LeapDownloaderConfig.with(saveDir: modelsDir, validateSha256: true) ) let runner = try await downloader.loadModel( @@ -146,7 +146,7 @@ Resolves the GGUF manifest for the given model + quantization slug, downloads an ```swift let downloader = LeapDownloader( - config: LeapDownloaderConfig(saveDir: modelsDir, validateSha256: true) + config: LeapDownloaderConfig.with(saveDir: modelsDir, validateSha256: true) ) let runner = try await downloader.loadModel( @@ -527,7 +527,7 @@ Per-load runtime overrides. Default values come from the model bundle's manifest .with(cacheOptions: .enabled(path: cacheDir.path)) ``` - **Sideloaded `LiquidInferenceEngineOptions` (URL-based load).** The non-manifest variant does NOT ship a Swift convenience init in v0.10.7 — the K/N-generated designated init takes all 12 fields. Either build it fully (verbose) or use `loadSimpleModel(model: ModelSource(...))` on `ModelDownloader` (preferred for new code; see the Sideloaded files section). The builder `.with(...)` overloads exist but they create a new instance internally via the same 12-arg init, so you still need a fully-built starting instance — there is no `LiquidInferenceEngineOptions(bundlePath: …)` 1-arg form today. + **Sideloaded `LiquidInferenceEngineOptions` (URL-based load).** The non-manifest variant does NOT ship a Swift convenience init through v0.10.9 — the K/N-generated designated init takes all 12 fields. Either build it fully (verbose) or use `loadSimpleModel(model: ModelSource(...))` on `ModelDownloader` (preferred for new code; see the Sideloaded files section). The builder `.with(...)` overloads exist but they create a new instance internally via the same 12-arg init, so you still need a fully-built starting instance — there is no `LiquidInferenceEngineOptions(bundlePath: …)` 1-arg form today. ```kotlin diff --git a/deployment/on-device/sdk/openai-client.mdx b/deployment/on-device/sdk/openai-client.mdx index b63416b..193176f 100644 --- a/deployment/on-device/sdk/openai-client.mdx +++ b/deployment/on-device/sdk/openai-client.mdx @@ -19,7 +19,7 @@ description: "Lightweight client for OpenAI-compatible chat completions APIs — ```swift dependencies: [ - .package(url: "https://github.com/Liquid4All/leap-sdk.git", from: "0.10.7") + .package(url: "https://github.com/Liquid4All/leap-sdk.git", from: "0.10.9") ] targets: [ @@ -37,8 +37,8 @@ description: "Lightweight client for OpenAI-compatible chat completions APIs — ```kotlin dependencies { - implementation("ai.liquid.leap:leap-sdk:0.10.7") - implementation("ai.liquid.leap:leap-openai-client:0.10.7") + implementation("ai.liquid.leap:leap-sdk:0.10.9") + implementation("ai.liquid.leap:leap-openai-client:0.10.9") } ``` @@ -47,18 +47,18 @@ description: "Lightweight client for OpenAI-compatible chat completions APIs — ```kotlin dependencies { - implementation("ai.liquid.leap:leap-sdk:0.10.7") - implementation("ai.liquid.leap:leap-openai-client:0.10.7") + implementation("ai.liquid.leap:leap-sdk:0.10.9") + implementation("ai.liquid.leap:leap-openai-client:0.10.9") } ``` - JVM support landed in v0.10.7 (the `jvm` slice was absent in the v0.10.0–v0.10.6 cascade). Pure-Maven JVM projects should consume the `-jvm` classifier directly: `ai.liquid.leap:leap-openai-client-jvm:0.10.7`. Bundles the CIO Ktor engine. + JVM support landed in v0.10.7 (the `jvm` slice was absent in the v0.10.0–v0.10.6 cascade). Pure-Maven JVM projects should consume the `-jvm` classifier directly: `ai.liquid.leap:leap-openai-client-jvm:0.10.9`. Bundles the CIO Ktor engine. ```kotlin dependencies { - implementation("ai.liquid.leap:leap-sdk:0.10.7") - implementation("ai.liquid.leap:leap-openai-client:0.10.7") + implementation("ai.liquid.leap:leap-sdk:0.10.9") + implementation("ai.liquid.leap:leap-openai-client:0.10.9") } ``` @@ -70,23 +70,14 @@ description: "Lightweight client for OpenAI-compatible chat completions APIs — - - The `leap-sdk-openai-client` Kotlin module does **not** apply the SKIE plugin in v0.10.7 (only `leap-sdk`, `leap-sdk-model-downloader`, and `leap-ui` do). That means `Flow` is **not** bridged to a Swift `AsyncSequence` and the `onEnum(of:)` helper is **not** generated for `ChatCompletionEvent`. Swift consumers on v0.10.7 must collect the Kotlin `Flow` through its native collector and downcast each event with `as?`. For most Swift apps that just need cloud chat completions, an off-the-shelf OpenAI Swift client is more ergonomic — use `LeapOpenAIClient` from Swift only if you need to share Kotlin code with Android. - - **Coming in the next release:** SKIE will be enabled on `leap-sdk-openai-client`, adding the same Swift-friendly surface as `LeapSDK` — `for try await event in client.streamChatCompletion(...)`, `onEnum(of: event)` exhaustive switching, and nested-class Swift names (`ChatCompletionEvent.Delta` instead of the current flattened `ChatCompletionEventDelta`). Swift convenience inits and builders for `OpenAiClientConfig` are also planned. Pin to v0.10.7 if you need the current behavior frozen; otherwise expect the more ergonomic surface to land soon. - - - Manual collection pattern (the `Flow.collect(...)` shape varies by Kotlin/Native version — check the framework header in your Xcode build for the exact label): + + **New in v0.10.8.** SKIE is now applied to `leap-sdk-openai-client`, matching `LeapSDK` / `LeapModelDownloader` / `LeapUI`. Swift consumers get a real `AsyncSequence`, exhaustive `onEnum(of:)` switching, nested Kotlin class names (`ChatCompletionEvent.Delta` instead of the previously flattened `ChatCompletionEventDelta`), and an `OpenAiClient(config:)` convenience init — no more `OpenAiClientKt.` prefix. If you need the pre-SKIE manual-collector surface frozen for some reason, pin to `0.10.7`; otherwise use the v0.10.8 surface below. + ```swift import LeapOpenAIClient - // The Kotlin top-level `fun OpenAiClient(config: OpenAiClientConfig)` exports as - // `OpenAiClientKt.OpenAiClient(config:)` (PascalCase preserved from the Kotlin - // function name). Without SKIE the K/N export also flattens Kotlin's nested - // class names — `ChatMessage.User` → `ChatMessageUser`, - // `ChatCompletionEvent.Delta` → `ChatCompletionEventDelta`, etc. - let client = OpenAiClientKt.OpenAiClient( + let client = OpenAiClient( config: OpenAiClientConfig( apiKey: "sk-…", baseUrl: "https://api.openai.com/v1" @@ -96,28 +87,31 @@ description: "Lightweight client for OpenAI-compatible chat completions APIs — let request = ChatCompletionRequest( model: "gpt-4o-mini", messages: [ - ChatMessageSystem(content: "You are a helpful assistant."), - ChatMessageUser(content: "What is the capital of Japan?") + ChatMessage.System(content: "You are a helpful assistant."), + ChatMessage.User(content: "What is the capital of Japan?") ], temperature: 0.7 ) - // Pseudocode — actual collector signature depends on your Kotlin/Native version - // and framework headers. Without SKIE, there is no `for try await` integration. - try await client.streamChatCompletion(request: request).collect( - collector: FlowCollector { event in - if let delta = event as? ChatCompletionEventDelta { - print(delta.content, terminator: "") - } else if let done = event as? ChatCompletionEventDone { - if let usage = done.usage { print("\nTokens: \(usage.totalTokens)") } - } else if let err = event as? ChatCompletionEventError { - print("\nError: \(err.message)") - } + for try await event in client.streamChatCompletion(request: request) { + switch onEnum(of: event) { + case .delta(let delta): + print(delta.content, terminator: "") + case .done(let done): + if let usage = done.usage { print("\nTokens: \(usage.totalTokens)") } + case .error(let err): + print("\nError: \(err.message)") } - ) + } client.close() // closes the underlying URLSession-backed HttpClient ``` + + `onEnum(of:)` gives exhaustive switching — the Swift compiler errors if a new `ChatCompletionEvent` case is added. + + + Errors are delivered **in-band**: a non-2xx HTTP response arrives as a `.error` event — handle it in the `switch` and show `err.message`. Malformed SSE chunks are logged and skipped (no event is emitted). Transport-level failures (network drop, TLS error) are *not* delivered to the bridged stream as `.error` events and are not rethrown to Swift, so a `do`/`catch` around the loop won't catch them — guard connectivity at a higher level (request timeouts, reachability checks) instead. + ```kotlin @@ -181,11 +175,7 @@ data class OpenAiClientConfig( ```swift - // The leap-sdk-openai-client module has no SKIE plugin applied, so the - // top-level Kotlin `fun OpenAiClient(config:)` factory is exported as - // `OpenAiClientKt.OpenAiClient(config:)`. See the [Basic usage](#basic-usage) - // warning for the full reasoning. - let client = OpenAiClientKt.OpenAiClient( + let client = OpenAiClient( config: OpenAiClientConfig( apiKey: "sk-or-…", baseUrl: "https://openrouter.ai/api/v1", @@ -218,7 +208,7 @@ data class OpenAiClientConfig( ```swift - let client = OpenAiClientKt.OpenAiClient( + let client = OpenAiClient( config: OpenAiClientConfig( apiKey: "anything", // Required by config but typically unused baseUrl: "http://10.0.0.42:8000/v1" @@ -266,17 +256,17 @@ data class ChatCompletionRequest( ) ``` -`ChatMessage` (the OpenAI-client one, distinct from `LeapSDK.ChatMessage`) is a sealed type with three cases — `System`, `User`, `Assistant`. +`ChatMessage` (the OpenAI-client one, distinct from the on-device `ChatMessage` in `LeapSDK` / `LeapModelDownloader`) is a sealed type with three cases — `System`, `User`, `Assistant`. ## Response shape -`streamChatCompletion(request)` returns a `Flow` (Kotlin) — and the same `Flow` is exposed verbatim to Swift in v0.10.7 (no SKIE on this module yet, so it's not bridged to a Swift `AsyncSequence`; collect it via the native `Flow.collect(...)` shape shown above). Events: +`streamChatCompletion(request)` returns a `Flow` (Kotlin) — SKIE bridges this as a Swift `AsyncSequence` since v0.10.8, so Swift consumers can iterate it with `for try await event in client.streamChatCompletion(request: ...)`. Events: | Variant | Meaning | |---|---| | `Delta(content: String)` | Text chunk from the model. May be empty for role-only deltas. | | `Done(usage: Usage?)` | Stream finished. `usage` is non-`null` when the API includes token counts. | -| `Error(message: String)` | HTTP error or stream parsing failure. | +| `Error(message: String)` | Non-2xx HTTP response. Malformed SSE chunks are logged and skipped — they do not emit an `Error` event. | ```kotlin data class Usage(val promptTokens: Int, val completionTokens: Int, val totalTokens: Int) @@ -304,24 +294,21 @@ Route simple prompts to a small on-device LFM; escalate harder prompts to a clou func send(_ text: String, useCloud: Bool) async throws { if useCloud { - // Cloud path: leap-sdk-openai-client has no SKIE — collect the Kotlin - // Flow manually and downcast each event with `as?`. Note the flattened - // Swift type names (`ChatMessageUser`, `ChatCompletionEventDelta`). + // Cloud path — same SKIE-bridged surface as on-device since v0.10.8. + // Both imported modules expose a `ChatMessage`, so qualify each by the + // module it comes from: `LeapOpenAIClient.ChatMessage` (cloud) and + // `LeapModelDownloader.ChatMessage` (on-device — LeapModelDownloader + // re-exports the LeapSDK types, so the type lives in that module here). let request = ChatCompletionRequest( model: "gpt-4o-mini", - messages: [ChatMessageUser(content: text)] - ) - try await cloud.streamChatCompletion(request: request).collect( - collector: FlowCollector { event in - if let delta = event as? ChatCompletionEventDelta { - appendChunk(delta.content) - } - } + messages: [LeapOpenAIClient.ChatMessage.User(content: text)] ) + for try await event in cloud.streamChatCompletion(request: request) { + if case let .delta(d) = onEnum(of: event) { appendChunk(d.content) } + } } else { - // On-device path: leap-sdk has SKIE — `for try await` + `onEnum(of:)` - // work as written. - let userMessage = ChatMessage(role: .user, textContent: text) + // On-device path. + let userMessage = LeapModelDownloader.ChatMessage(role: .user, textContent: text) for try await response in onDevice.generateResponse(message: userMessage) { if case let .chunk(c) = onEnum(of: response) { appendChunk(c.text) } } @@ -412,7 +399,7 @@ See [Cloud AI Comparison](./cloud-ai-comparison) for a side-by-side feature brea ## Lifecycle -The platform `OpenAiClient(config:)` factory (Kotlin `fun OpenAiClient(config:)` → Swift `OpenAiClientKt.OpenAiClient(config:)`) creates an `HttpClient` internally and ties it to the returned client — call `close()` when you're done. +The `OpenAiClient(config:)` factory (Kotlin `fun OpenAiClient(config:)` — exported as a SKIE-bundled Swift convenience init since v0.10.8) creates an `HttpClient` internally and ties it to the returned client — call `close()` when you're done. @@ -420,7 +407,7 @@ The platform `OpenAiClient(config:)` factory (Kotlin `fun OpenAiClient(config:)` deinit { client.close() } ``` - The lower-level constructor that accepts an externally-managed `HttpClient` is part of the Kotlin/Ktor surface and isn't a useful entry point from Swift — the Ktor engine machinery isn't bridged into the public Swift API. Use `OpenAiClientKt.OpenAiClient(config:)` and let the SDK own the session. If multiple consumers share a client, share the `OpenAiClient` instance and `close()` once at teardown. + The lower-level constructor that accepts an externally-managed `HttpClient` is part of the Kotlin/Ktor surface and isn't a useful entry point from Swift — the Ktor engine machinery isn't bridged into the public Swift API. Use `OpenAiClient(config:)` and let the SDK own the session. If multiple consumers share a client, share the `OpenAiClient` instance and `close()` once at teardown. ```kotlin diff --git a/deployment/on-device/sdk/quick-start.mdx b/deployment/on-device/sdk/quick-start.mdx index 348c902..c6cb397 100644 --- a/deployment/on-device/sdk/quick-start.mdx +++ b/deployment/on-device/sdk/quick-start.mdx @@ -3,7 +3,7 @@ title: "Quick Start" description: "Install the LEAP SDK on iOS, macOS, Android, JVM, Linux, or Windows — same API everywhere." --- -Latest version: `v0.10.7` +Latest version: `v0.10.9` The Leap SDK is a Kotlin Multiplatform library: the same `ModelRunner` / `Conversation` / `MessageResponse` API runs on every supported target. The code differs only in **language** (Swift vs. Kotlin) and **packaging** (SPM, Gradle, or Kotlin/Native plugin) — the call shapes are identical. For background on what the SDK is and what first-class LFM support means in practice, see the [Overview](/deployment/on-device/sdk/overview). @@ -29,7 +29,7 @@ The Leap SDK is a Kotlin Multiplatform library: the same `ModelRunner` / `Conver plugins { id("com.android.application") version "8.13.2" apply false id("com.android.library") version "8.13.2" apply false - id("org.jetbrains.kotlin.android") version "2.3.20" apply false + id("org.jetbrains.kotlin.android") version "2.3.21" apply false } ``` - A working Android device that supports `arm64-v8a` with [developer mode enabled](https://developer.android.com/studio/debug/dev-options) and 3 GB+ of RAM. @@ -50,7 +50,7 @@ The Leap SDK is a Kotlin Multiplatform library: the same `ModelRunner` / `Conver The `leap-sdk` JAR bundles the JNI binaries for every supported OS/arch — no extra setup needed. - - Kotlin **2.3.20+** and Gradle 8.x for the Kotlin/Native build. + - Kotlin **2.3.21+** and Gradle 8.x for the Kotlin/Native build. - **Linux runtime:** glibc **2.34+** (Ubuntu 22.04, Debian 12, RHEL 9, or newer). - **Windows runtime:** Windows 10+. @@ -68,7 +68,7 @@ The Leap SDK is a Kotlin Multiplatform library: the same `ModelRunner` / `Conver 1. In Xcode choose **File → Add Package Dependencies**. 2. Enter `https://github.com/Liquid4All/leap-sdk.git`. - 3. Select the `0.10.7` release (or newer). + 3. Select the `0.10.9` release (or newer). 4. Add the products you need to your app target. The package vends five products. Most apps only need one or two: @@ -94,7 +94,7 @@ The Leap SDK is a Kotlin Multiplatform library: the same `ModelRunner` / `Conver - For explicit pinning, declare each framework as a `.binaryTarget` in your `Package.swift`. The XCFramework assets live on the `Liquid4All/leap-sdk` v0.10.7 release page — copy the SHA-256 values from there. + For explicit pinning, declare each framework as a `.binaryTarget` in your `Package.swift`. The XCFramework assets live on the `Liquid4All/leap-sdk` v0.10.9 release page — copy the SHA-256 values from there. The constrained-generation macros (`@Generatable`, `@Guide`) are Swift macros, not XCFrameworks — they ship as the `LeapSDKMacros` source target inside the SPM package and **cannot be installed as a `.binaryTarget`**. If you need them, use the standard SPM package URL above (or add the `LeapSDKMacros` source target separately on top of your binary targets). @@ -103,23 +103,23 @@ The Leap SDK is a Kotlin Multiplatform library: the same `ModelRunner` / `Conver ```swift .binaryTarget( name: "LeapSDK", - url: "https://github.com/Liquid4All/leap-sdk/releases/download/v0.10.7/LeapSDK.xcframework.zip", - checksum: "6f2721aa45d7555646f78cbcaedb57aba3d869f56b24d681ad332846e131ae3d" + url: "https://github.com/Liquid4All/leap-sdk/releases/download/v0.10.9/LeapSDK.xcframework.zip", + checksum: "1fdea67fe208ee56db3aba4313809b03a469e6520a49a855c212ebcfef4cea2e" ), .binaryTarget( name: "LeapModelDownloader", - url: "https://github.com/Liquid4All/leap-sdk/releases/download/v0.10.7/LeapModelDownloader.xcframework.zip", - checksum: "f649aa6c1aa3e87bbeb1073d5aeeb7224879359a24b18eeccc665d24abc725d8" + url: "https://github.com/Liquid4All/leap-sdk/releases/download/v0.10.9/LeapModelDownloader.xcframework.zip", + checksum: "41bc9a67b1a70c6b5bc42cc1433feb4c346b9f3ecb6bc831eae4ae8f81cdb2a0" ), .binaryTarget( name: "LeapOpenAIClient", - url: "https://github.com/Liquid4All/leap-sdk/releases/download/v0.10.7/LeapOpenAIClient.xcframework.zip", - checksum: "79bc5443a1cce6fcd4c49c91eeb85727034aaca10d3ef69582c061989c3d9b70" + url: "https://github.com/Liquid4All/leap-sdk/releases/download/v0.10.9/LeapOpenAIClient.xcframework.zip", + checksum: "fabdc0ebc3f355676271f6811bfe736af2b4486b49afdb344d2596108cb25a8e" ), .binaryTarget( name: "LeapUi", - url: "https://github.com/Liquid4All/leap-sdk/releases/download/v0.10.7/LeapUi.xcframework.zip", - checksum: "f1b198cef88c2a37eaf6dc1f36395d6aed024b0c6c2b43724d942e25b60d22e0" + url: "https://github.com/Liquid4All/leap-sdk/releases/download/v0.10.9/LeapUi.xcframework.zip", + checksum: "72247e66ce53ba9439f75af339540570172edd7f26ad2621db022889e23af696" ), ``` @@ -131,14 +131,14 @@ The Leap SDK is a Kotlin Multiplatform library: the same `ModelRunner` / `Conver ```kotlin dependencies { - implementation("ai.liquid.leap:leap-sdk:0.10.7") - implementation("ai.liquid.leap:leap-model-downloader:0.10.7") // Android background downloads + implementation("ai.liquid.leap:leap-sdk:0.10.9") + implementation("ai.liquid.leap:leap-model-downloader:0.10.9") // Android background downloads // Optional: OpenAI-compatible cloud chat client - // implementation("ai.liquid.leap:leap-openai-client:0.10.7") + // implementation("ai.liquid.leap:leap-openai-client:0.10.9") // Optional: Voice assistant widget (Compose Multiplatform) - // implementation("ai.liquid.leap:leap-ui:0.10.7") + // implementation("ai.liquid.leap:leap-ui:0.10.9") } ``` @@ -147,7 +147,7 @@ The Leap SDK is a Kotlin Multiplatform library: the same `ModelRunner` / `Conver ```toml [versions] - leapSdk = "0.10.7" + leapSdk = "0.10.9" [libraries] leap-sdk = { module = "ai.liquid.leap:leap-sdk", version.ref = "leapSdk" } @@ -182,7 +182,7 @@ The Leap SDK is a Kotlin Multiplatform library: the same `ModelRunner` / `Conver ```kotlin plugins { - kotlin("jvm") version "2.3.20" + kotlin("jvm") version "2.3.21" application } @@ -191,11 +191,11 @@ The Leap SDK is a Kotlin Multiplatform library: the same `ModelRunner` / `Conver } dependencies { - implementation("ai.liquid.leap:leap-sdk:0.10.7") + implementation("ai.liquid.leap:leap-sdk:0.10.9") // Optional: - // implementation("ai.liquid.leap:leap-openai-client:0.10.7") - // implementation("ai.liquid.leap:leap-ui:0.10.7") // Compose for Desktop voice widget + // implementation("ai.liquid.leap:leap-openai-client:0.10.9") + // implementation("ai.liquid.leap:leap-ui:0.10.9") // Compose for Desktop voice widget } ``` @@ -221,12 +221,12 @@ The Leap SDK is a Kotlin Multiplatform library: the same `ModelRunner` / `Conver ```kotlin // build.gradle.kts plugins { - kotlin("multiplatform") version "2.3.20" - id("ai.liquid.leap.nativelibs") version "0.10.7" + kotlin("multiplatform") version "2.3.21" + id("ai.liquid.leap.nativelibs") version "0.10.9" } dependencies { - implementation("ai.liquid.leap:leap-sdk:0.10.7") + implementation("ai.liquid.leap:leap-sdk:0.10.9") } kotlin { @@ -260,7 +260,7 @@ The recommended path is **manifest-based** loading. On every platform, the platf return (caches as NSString).appendingPathComponent("leap_models") }() private lazy var downloader = ModelDownloader( - config: LeapDownloaderConfig(saveDir: modelsDir) + config: LeapDownloaderConfig.with(saveDir: modelsDir) // For background transfers, pass: // sessionConfiguration: .background(withIdentifier: "com.myapp.leap.downloads") ) @@ -378,7 +378,10 @@ The recommended path is **manifest-based** loading. On every platform, the platf when (resp) { is MessageResponse.Chunk -> print(resp.text) is MessageResponse.Complete -> println("\n[done]") - else -> {} + is MessageResponse.ReasoningChunk -> {} + is MessageResponse.FunctionCalls -> {} + is MessageResponse.AudioSample -> {} + is MessageResponse.Error -> System.err.println("\n[error] ${resp.message}") } } @@ -473,6 +476,11 @@ Both platforms expose the same streaming shape: an async sequence of `MessageRes return nil }.joined() print("Final:", text) + case .error(let err): + // In-band failure (v0.10.8+) — the SKIE-bridged stream does not rethrow the + // Kotlin exception to Swift, so generation failures arrive here, not on a + // `catch` block around the loop. + print("Generation failed:", err.message) } } ``` @@ -489,6 +497,7 @@ Both platforms expose the same streaming shape: an async sequence of `MessageRes is MessageResponse.FunctionCalls -> handleFunctionCalls(response.functionCalls) is MessageResponse.AudioSample -> audioRenderer.enqueue(response.samples, response.sampleRate) is MessageResponse.Complete -> Log.d(TAG, "Done. Stats: ${response.stats}") + is MessageResponse.Error -> _errorMessage.value = "Generation failed: ${response.message}" } } ?.onCompletion { _isGenerating.value = false } diff --git a/deployment/on-device/sdk/utilities.mdx b/deployment/on-device/sdk/utilities.mdx index e57ad7c..aca186c 100644 --- a/deployment/on-device/sdk/utilities.mdx +++ b/deployment/on-device/sdk/utilities.mdx @@ -61,7 +61,7 @@ This page covers error types, serialization helpers, and a few platform-specific ```kotlin plugins { - id("org.jetbrains.kotlin.plugin.serialization") version "2.3.20" + id("org.jetbrains.kotlin.plugin.serialization") version "2.3.21" } dependencies { @@ -210,7 +210,7 @@ A minimal end-to-end snippet exercising load → conversation → tool registrat ```swift let caches = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first!.path let modelsDir = (caches as NSString).appendingPathComponent("leap_models") - let downloader = ModelDownloader(config: LeapDownloaderConfig(saveDir: modelsDir)) + let downloader = ModelDownloader(config: LeapDownloaderConfig.with(saveDir: modelsDir)) let runner = try await downloader.loadModel( modelName: "LFM2.5-1.2B-Instruct", diff --git a/deployment/on-device/sdk/voice-assistant.mdx b/deployment/on-device/sdk/voice-assistant.mdx index 14f1884..fcfcd4d 100644 --- a/deployment/on-device/sdk/voice-assistant.mdx +++ b/deployment/on-device/sdk/voice-assistant.mdx @@ -11,7 +11,7 @@ The `leap-ui` module (introduced in v0.10.0) ships a ready-to-use voice assistan - **macOS** — bridged to AppKit via `VoiceAssistantNSViewController`. SwiftUI hosts via `NSViewControllerRepresentable` + `NSHostingController`. - **Android** — direct Compose for Android. - **JVM Desktop** — Compose for Desktop. Same Maven artifact; you provide audio I/O implementations (the demo apps in `leap-ui-demo/` ship patterns you can adapt). -- **Web (Wasm, experimental)** — present in the source tree (`leap-ui-demo/web`) but not yet covered by the stable release notes through v0.10.7 — treat as preview. +- **Web (Wasm, experimental)** — present in the source tree (`leap-ui-demo/web`) but not yet covered by the stable release notes through v0.10.9 — treat as preview. ## Add the dependency @@ -21,7 +21,7 @@ The `leap-ui` module (introduced in v0.10.0) ships a ready-to-use voice assistan ```swift dependencies: [ - .package(url: "https://github.com/Liquid4All/leap-sdk.git", from: "0.10.7") + .package(url: "https://github.com/Liquid4All/leap-sdk.git", from: "0.10.9") ] targets: [ @@ -46,8 +46,8 @@ The `leap-ui` module (introduced in v0.10.0) ships a ready-to-use voice assistan ```kotlin dependencies { - implementation("ai.liquid.leap:leap-sdk:0.10.7") - implementation("ai.liquid.leap:leap-ui:0.10.7") + implementation("ai.liquid.leap:leap-sdk:0.10.9") + implementation("ai.liquid.leap:leap-ui:0.10.9") } ``` @@ -87,7 +87,7 @@ The `VoiceConversation` adapter looks similar on every platform — both impleme private let downloader: ModelDownloader = { let caches = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first!.path let modelsDir = (caches as NSString).appendingPathComponent("leap_models") - return ModelDownloader(config: LeapDownloaderConfig(saveDir: modelsDir)) + return ModelDownloader(config: LeapDownloaderConfig.with(saveDir: modelsDir)) }() init() { @@ -348,6 +348,9 @@ The store calls into a `VoiceConversation` you provide. A minimal adapter that w stats = c.stats case .chunk, .reasoningChunk, .functionCalls: break + case .error(let err): + // Surface generation failure to the voice store — playback stops gracefully. + throw NSError(domain: "LeapVoice", code: 1, userInfo: [NSLocalizedDescriptionKey: err.message]) } } return stats @@ -389,7 +392,10 @@ The store calls into a `VoiceConversation` you provide. A minimal adapter that w when (response) { is MessageResponse.AudioSample -> onAudioChunk(response.samples, response.sampleRate) is MessageResponse.Complete -> stats = response.stats - else -> Unit + is MessageResponse.Chunk -> {} + is MessageResponse.ReasoningChunk -> {} + is MessageResponse.FunctionCalls -> {} + is MessageResponse.Error -> throw response.throwable } } return stats diff --git a/examples/android/leap-koog-agent.mdx b/examples/android/leap-koog-agent.mdx index 4049c64..68f1a8d 100644 --- a/examples/android/leap-koog-agent.mdx +++ b/examples/android/leap-koog-agent.mdx @@ -106,8 +106,8 @@ Before running this example, ensure you have the following: ```kotlin dependencies { // LeapSDK for on-device AI (0.10.0+) - implementation("ai.liquid.leap:leap-sdk:0.10.7") - implementation("ai.liquid.leap:leap-model-downloader:0.10.7") + implementation("ai.liquid.leap:leap-sdk:0.10.9") + implementation("ai.liquid.leap:leap-model-downloader:0.10.9") // Koog framework for AI agents implementation("ai.koog:koog-agents:0.5.0") diff --git a/examples/android/recipe-generator-constrained-output.mdx b/examples/android/recipe-generator-constrained-output.mdx index 67efce0..6bde02e 100644 --- a/examples/android/recipe-generator-constrained-output.mdx +++ b/examples/android/recipe-generator-constrained-output.mdx @@ -105,8 +105,8 @@ Before running this example, ensure you have the following: ```kotlin dependencies { // LeapSDK + the Android downloader module - implementation("ai.liquid.leap:leap-sdk:0.10.7") - implementation("ai.liquid.leap:leap-model-downloader:0.10.7") + implementation("ai.liquid.leap:leap-sdk:0.10.9") + implementation("ai.liquid.leap:leap-model-downloader:0.10.9") // Kotlin serialization for type-safe parsing implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0") @@ -125,7 +125,7 @@ Before running this example, ensure you have the following: ```kotlin plugins { - id("org.jetbrains.kotlin.plugin.serialization") version "2.3.20" + id("org.jetbrains.kotlin.plugin.serialization") version "2.3.21" } ``` @@ -148,7 +148,7 @@ Follow these steps to generate structured recipes: 3. **Gradle sync** - Wait for Gradle to sync all dependencies - - Ensure LeapSDK 0.10.7 is downloaded + - Ensure LeapSDK 0.10.9 is downloaded 4. **Run the app** - Connect your Android device or start an emulator diff --git a/examples/android/slogan-generator.mdx b/examples/android/slogan-generator.mdx index 0bc295d..d7b8c09 100644 --- a/examples/android/slogan-generator.mdx +++ b/examples/android/slogan-generator.mdx @@ -47,8 +47,8 @@ Before running this example, ensure you have the following: ```kotlin dependencies { - implementation("ai.liquid.leap:leap-sdk:0.10.7") - implementation("ai.liquid.leap:leap-model-downloader:0.10.7") + implementation("ai.liquid.leap:leap-sdk:0.10.9") + implementation("ai.liquid.leap:leap-model-downloader:0.10.9") // Android UI components implementation("androidx.appcompat:appcompat:1.6.1") diff --git a/examples/android/vision-language-model-example.mdx b/examples/android/vision-language-model-example.mdx index 3b8f758..be4d87b 100644 --- a/examples/android/vision-language-model-example.mdx +++ b/examples/android/vision-language-model-example.mdx @@ -92,8 +92,8 @@ Before running this example, ensure you have the following: ```kotlin dependencies { // LeapSDK for VLM processing (0.10.0+) - implementation("ai.liquid.leap:leap-sdk:0.10.7") - implementation("ai.liquid.leap:leap-model-downloader:0.10.7") + implementation("ai.liquid.leap:leap-sdk:0.10.9") + implementation("ai.liquid.leap:leap-model-downloader:0.10.9") // Coil for image loading implementation("io.coil-kt:coil-compose:2.5.0") diff --git a/examples/android/web-content-summarizer.mdx b/examples/android/web-content-summarizer.mdx index ab85ae3..dbffae0 100644 --- a/examples/android/web-content-summarizer.mdx +++ b/examples/android/web-content-summarizer.mdx @@ -61,8 +61,8 @@ Before running this example, ensure you have the following: ```kotlin dependencies { // LeapSDK for AI processing (0.10.0+) - implementation("ai.liquid.leap:leap-sdk:0.10.7") - implementation("ai.liquid.leap:leap-model-downloader:0.10.7") + implementation("ai.liquid.leap:leap-sdk:0.10.9") + implementation("ai.liquid.leap:leap-model-downloader:0.10.9") // Networking for web scraping implementation("com.squareup.okhttp3:okhttp:4.12.0")