From 9e13edbbc2883c0d6f5f7442b417ef1aedcc68e8 Mon Sep 17 00:00:00 2001 From: David Berrios Date: Thu, 21 May 2026 09:46:49 -0700 Subject: [PATCH 1/7] docs(leap-sdk): bump example dependency pins to 0.10.8 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mechanical version bump across the five Android example tutorials — `ai.liquid.leap:leap-sdk` and `ai.liquid.leap:leap-model-downloader` Gradle pins from `0.10.7` → `0.10.8`. No prose changes; the example snippets continue to demonstrate the same APIs. --- examples/android/leap-koog-agent.mdx | 4 ++-- examples/android/recipe-generator-constrained-output.mdx | 6 +++--- examples/android/slogan-generator.mdx | 4 ++-- examples/android/vision-language-model-example.mdx | 4 ++-- examples/android/web-content-summarizer.mdx | 4 ++-- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/examples/android/leap-koog-agent.mdx b/examples/android/leap-koog-agent.mdx index 4049c64..e08bcba 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.8") + implementation("ai.liquid.leap:leap-model-downloader:0.10.8") // 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..4e93330 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.8") + implementation("ai.liquid.leap:leap-model-downloader:0.10.8") // Kotlin serialization for type-safe parsing implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0") @@ -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.8 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..4be32e3 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.8") + implementation("ai.liquid.leap:leap-model-downloader:0.10.8") // 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..5189364 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.8") + implementation("ai.liquid.leap:leap-model-downloader:0.10.8") // 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..9003654 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.8") + implementation("ai.liquid.leap:leap-model-downloader:0.10.8") // Networking for web scraping implementation("com.squareup.okhttp3:okhttp:4.12.0") From 501f47efd4dcf37c387152fe95a6b95be879cdf8 Mon Sep 17 00:00:00 2001 From: David Berrios Date: Thu, 21 May 2026 09:47:19 -0700 Subject: [PATCH 2/7] docs(leap-sdk): add v0.10.8 changelog + document MessageResponse.Error MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add the v0.10.8 release entry to the SDK changelog and the corresponding reference-doc updates for the new in-band failure event. Changelog (`leap-sdk-changelog.mdx`): - Bump headline "Latest release" v0.10.7 → v0.10.8. - Convert the v0.10.7 "next release will enable SKIE..." teaser into a past-tense pointer at the new v0.10.8 section so the v0.10.7 entry stays internally consistent. - New `### v0.10.8 — 2026-05-21` section covering: - SKIE applied to `leap-sdk-openai-client` — `client.streamChatCompletion` is now a Swift `AsyncSequence`, `onEnum(of:)` switches exhaustively over `ChatCompletionEvent`, nested Kotlin class names are preserved (`ChatCompletionEvent.Delta` instead of K/N-flattened `ChatCompletionEventDelta`), and the top-level `fun OpenAiClient(config:)` factory exports as a real Swift convenience init (no more `OpenAiClientKt.` prefix). The framework is now distributable so a plain `import LeapOpenAIClient` is enough. - New in-band `MessageResponse.Error(throwable, message)` / `LiquidMessageResponse.Error` — closes the Swift `Failure = Never` gap on `Conversation.generateResponse(...)`. Kotlin consumers keep working unchanged because the channel still closes with the underlying throwable. - Exhaustive-`when` / `switch` migration notes for both Kotlin and Swift call sites. - Terminal-operator caveat warning — `first()` / `first { ... }` / `take(1)` may now consume the in-band `Error` value and complete *normally*, where they previously propagated exceptionally. - The `Flow` is now backed by `buffer(Channel.UNLIMITED)` so the terminal `Error` emission can never be silently dropped under collector backpressure. - Toolchain: Kotlin 2.3.20 → 2.3.21, SKIE 0.10.11 → 0.10.12. No Xcode or Swift baseline change. `MessageResponse` reference (`conversation-generation.mdx`): - Add the new sealed-class case to the Swift and Kotlin reference blocks: Swift `case error(Error)`; Kotlin `data class Error(throwable, message)` with the documented `message` default of `throwable.message ?: throwable::class.simpleName ?: throwable.toString()`. - Add a bulleted entry documenting the new case alongside the existing ones, including the reference-equality-on-throwable caveat (since `Throwable` does not override `equals` / `hashCode`, the synthesised `data class` equality is reference-based). - Update the streaming-generation Swift example's `switch onEnum(of: response)` with a `case .error(let err)` arm that explains the SKIE `Failure = Never` rationale inline. - Update the streaming-generation Kotlin example's `when (response)` with an `is MessageResponse.Error -> Log.e(...)` arm. - Rewrite the trailing "Errors propagate as `LeapGenerationException`" note into a two-paragraph explanation covering the dual error path (in-band `Error` value + exceptional flow close) and adding a `` callout for the terminal-operator caveat. --- deployment/on-device/leap-sdk-changelog.mdx | 53 ++++++++++++++++++- .../on-device/sdk/conversation-generation.mdx | 22 +++++++- 2 files changed, 71 insertions(+), 4 deletions(-) diff --git a/deployment/on-device/leap-sdk-changelog.mdx b/deployment/on-device/leap-sdk-changelog.mdx index 0c1a4d7..27ad4bb 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.8** ([GitHub](https://github.com/Liquid4All/leap-sdk/releases/tag/v0.10.8)). 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,55 @@ val runner = downloader.loadModel( ## Per-release notes +### 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` with `Failure = Never`, so a thrown `LeapGenerationException` could not be observed from Swift — `for await response in conversation.generateResponse(...)` simply terminated silently. 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 +205,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/conversation-generation.mdx b/deployment/on-device/sdk/conversation-generation.mdx index e9815a2..09a4d52 100644 --- a/deployment/on-device/sdk/conversation-generation.mdx +++ b/deployment/on-device/sdk/conversation-generation.mdx @@ -168,6 +168,12 @@ 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): + // SKIE bridges `Flow` as an `AsyncSequence` with `Failure = Never`, so the + // surrounding `for try await` cannot observe a thrown error from the Kotlin + // side. Generation failures surface as this in-band `.error` case (added in + // v0.10.8) — handle it here and present `err.message` to the user. + print("\nGeneration failed:", err.message) } } } catch { @@ -199,6 +205,7 @@ 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) } @@ -216,10 +223,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, which see a SKIE-bridged `AsyncSequence` with `Failure = Never`, can react), **and** as a `LeapGenerationException` thrown when the flow closes (so `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 +270,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 +288,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 +302,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 (which see a SKIE-bridged `AsyncSequence` with `Failure = Never`) 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` From 3f753161852fa3120231ea435e276635af124ab9 Mon Sep 17 00:00:00 2001 From: David Berrios Date: Thu, 21 May 2026 09:47:41 -0700 Subject: [PATCH 3/7] docs(openai-client): rewrite Swift surface for v0.10.8 SKIE bridge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit v0.10.8 applies SKIE to `leap-sdk-openai-client`, so the entire Swift side of the OpenAI-compatible client page needed to be rewritten — the v0.10.7 docs described the manual `Flow.collect(collector: FlowCollector { ... })` shape with K/N-flattened type names. Two source-accuracy gaps in the prior version also fixed in this pass. SKIE surface rewrites: - Replaced the v0.10.7 "no SKIE / manual collector" `` with a v0.10.8 `` flip explaining the new surface and pointing readers who want the old behavior frozen to a `0.10.7` pin. - Rewrote every Swift code block to use the new ergonomic surface: `OpenAiClient(config: ...)` instead of `OpenAiClientKt.OpenAiClient(config: ...)`; `for try await event in client.streamChatCompletion(request: ...)` with `switch onEnum(of: event) { case .delta / .done / .error }` instead of the manual `FlowCollector`. Affected blocks: Basic usage, OpenRouter configuration, self-hosted vLLM / llama-server, hybrid on-device + cloud routing. - Switched all `ChatMessage` constructions from K/N-flattened `ChatMessageUser(content:)` / `ChatMessageSystem(content:)` to the SKIE-preserved nested form `ChatMessage.User(content:)` / `ChatMessage.System(content:)` — confirmed against the source `sealed interface ChatMessage { data class System, User, Assistant }`. - Updated the Response-shape prose paragraph to drop the no-SKIE-bridge caveat — Swift consumers see a `AsyncSequence` now. - Lifecycle section's Swift block dropped the `OpenAiClientKt.` prefix references; the SKIE-bundled `OpenAiClient(config:)` is the Swift entry point. Bumped every dependency pin in the install-the-SDK Tabs from 0.10.7 → 0.10.8 (SPM `from:`, Android Gradle, JVM Gradle, Kotlin/Native Gradle). Source-accuracy gaps caught while rewriting: 1. The Basic-usage Swift sample previously wrapped the `for try await` in a `do/catch` with a comment claiming that "transport-level failures (network drop, bad TLS, etc.) surface as a thrown error on the AsyncSequence." That claim is **wrong under SKIE FlowInterop with `Failure = Never`** — transport errors silently terminate the `AsyncSequence` rather than throwing. Dropped the misleading `do/catch` and added a `` explaining the silent-termination behavior, plus the "track whether `.done` was observed" workaround for detecting it. 2. The Response-shape table's `Error(message:)` row claimed it covers "HTTP error or stream parsing failure." Source contradicts the second half: `OpenAiClient.processChunk(...)` catches `Exception` from `json.decodeFromString` and `log.w { "Skipping malformed SSE chunk: ${e.message}" }; null` — malformed chunks are silently skipped, not surfaced. Updated the row to "Non-2xx HTTP response. Malformed SSE chunks are logged and skipped — they do not emit an `Error` event." --- deployment/on-device/sdk/openai-client.mdx | 101 +++++++++------------ 1 file changed, 42 insertions(+), 59 deletions(-) diff --git a/deployment/on-device/sdk/openai-client.mdx b/deployment/on-device/sdk/openai-client.mdx index b63416b..23b61a2 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.8") ] 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.8") + implementation("ai.liquid.leap:leap-openai-client:0.10.8") } ``` @@ -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.8") + implementation("ai.liquid.leap:leap-openai-client:0.10.8") } ``` - 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.8`. 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.8") + implementation("ai.liquid.leap:leap-openai-client:0.10.8") } ``` @@ -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. + + + SKIE bridges Kotlin `Flow` to a Swift `AsyncSequence` with `Failure = Never`, so transport-level failures (network drop, TLS error, etc.) silently terminate the stream rather than throwing. Only HTTP-level errors (non-2xx response) arrive as in-band `.error` events; malformed SSE chunks are logged and skipped. If you need to react to silent termination, track whether `.done` was observed and treat a stream that ended without it as a transport failure. + ```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" @@ -270,13 +260,13 @@ data class ChatCompletionRequest( ## 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,17 @@ 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. 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: [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 = LeapSDK.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 +395,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 +403,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 From b9c1a82e7fe30dd082e9ff684d19ce72cd97f82f Mon Sep 17 00:00:00 2001 From: David Berrios Date: Thu, 21 May 2026 09:48:11 -0700 Subject: [PATCH 4/7] docs(leap-sdk): bump SDK page version pins + exhaustive MessageResponse switches MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two related changes across the remaining SDK pages: 1. Version pins bumped 0.10.7 → 0.10.8 — every `ai.liquid.leap:*` Gradle coordinate, SPM `from: "0.10.7"`, `nativelibs` plugin version, the `gradle/libs.versions.toml` snippet, Maven `` block, XCFramework binary-target URLs, and `*-natives@zip` classifier coordinates. Quick-start's XCFramework SHA-256 values were replaced with `` placeholders since the v0.10.8 GitHub release wasn't live when these were drafted; a follow-up pass needs to drop in the real checksums. 2. Every Kotlin `when (response)` over `MessageResponse` and every Swift `switch onEnum(of: response)` that previously used an `else -> {}` / `else -> Unit` / `default: break` fallback was expanded into explicit arms covering all six `MessageResponse` subtypes (`Chunk`, `ReasoningChunk`, `FunctionCalls`, `AudioSample`, `Complete`, and the new `Error` case from v0.10.8). Rationale: the example code is what readers copy into their apps; under v0.10.8 a fallback-based pattern silently swallows the new `Error` arm on Kotlin (statement-`when` compiles with a warning, expression-`when` won't) and on Swift the compiler enforces exhaustiveness, so call sites stop compiling. Showing the explicit `.error` arm in every example puts the migration path right next to the surrounding code. Per-file detail: - `quick-start.mdx` — bump every dependency pin; XCFramework binary-target SHA-256s replaced with placeholders; iOS Swift streaming example gains `case .error(let err)` arm; Android Kotlin `when` gains `is MessageResponse.Error -> _errorMessage.value = ...` arm; Kotlin/Native quick-start `when` expanded from 2-arm + `else` to 6 explicit arms with `Error → System.err.println(...)`. - `ai-agent-usage-guide.mdx` — first handler example: Swift `switch onEnum(of:)` gains `case .error(let err): log(...)`; Kotlin `when` gains `is MessageResponse.Error -> Log.e(...)`. ChatViewModel example: same treatment, Kotlin path writes to `_errorMessage` so SwiftUI / Compose can surface it. Agent-loop example: Swift `default: break` replaced with `case .reasoningChunk, .audioSample: break` + `case .error(let err): throw NSError(...)`; Kotlin `else -> {}` replaced with explicit no-op arms plus `is MessageResponse.Error -> throw response.throwable`. - `function-calling.mdx` — Swift exhaustive case-list gains `.error` arm; Kotlin `when` expanded with explicit `ReasoningChunk` / `AudioSample` no-ops plus `Error → Log.e(...)`. - `cloud-ai-comparison.mdx` — Swift case-list gains `.error` arm surfacing via `print(...)`; Kotlin `when` expanded to 6 arms. - `voice-assistant.mdx` — bump SPM `from:` and Gradle pins; bump "stable release notes through v0.10.7" → "through v0.10.8"; Swift audio-streaming `switch` gains `case .error(let err): throw NSError(...)` so the surrounding `VoiceConversation` machinery surfaces the failure; Kotlin `when` expanded with explicit no-op arms plus `Error → throw response.throwable` matching the Swift shape. - `desktop-platforms.mdx` — bump every dependency pin, plugin version, Maven ``, natives-zip coords, and the XCFramework binary-target URL; Kotlin K/N quick-start `when` expanded to 6 arms. - `model-loading.mdx` — rewrite the "Sideloaded `LiquidInferenceEngineOptions` (URL-based load)" callout from "does NOT ship a Swift convenience init in v0.10.7" to "...through v0.10.8" so the version stays accurate — v0.10.8's SKIE-on-openai-client change didn't touch `LiquidInferenceEngineOptions`, so the all-12- fields situation is unchanged. --- .../on-device/sdk/ai-agent-usage-guide.mdx | 15 +++++- .../on-device/sdk/cloud-ai-comparison.mdx | 7 ++- .../on-device/sdk/desktop-platforms.mdx | 39 +++++++------- deployment/on-device/sdk/function-calling.mdx | 6 ++- deployment/on-device/sdk/model-loading.mdx | 2 +- deployment/on-device/sdk/quick-start.mdx | 52 +++++++++++-------- deployment/on-device/sdk/voice-assistant.mdx | 16 ++++-- 7 files changed, 87 insertions(+), 50 deletions(-) diff --git a/deployment/on-device/sdk/ai-agent-usage-guide.mdx b/deployment/on-device/sdk/ai-agent-usage-guide.mdx index bd117c2..2337941 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 } } @@ -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..02030ae 100644 --- a/deployment/on-device/sdk/cloud-ai-comparison.mdx +++ b/deployment/on-device/sdk/cloud-ai-comparison.mdx @@ -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() ``` diff --git a/deployment/on-device/sdk/desktop-platforms.mdx b/deployment/on-device/sdk/desktop-platforms.mdx index 26ca1c6..5cc0d44 100644 --- a/deployment/on-device/sdk/desktop-platforms.mdx +++ b/deployment/on-device/sdk/desktop-platforms.mdx @@ -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.8") // 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.8") // 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.8") } 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.8' } 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.8 ``` @@ -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}") } } @@ -194,11 +197,11 @@ dependencyResolutionManagement { // build.gradle.kts plugins { kotlin("multiplatform") version "2.3.20" - id("ai.liquid.leap.nativelibs") version "0.10.7" + id("ai.liquid.leap.nativelibs") version "0.10.8" } dependencies { - implementation("ai.liquid.leap:leap-sdk:0.10.7") + implementation("ai.liquid.leap:leap-sdk:0.10.8") } kotlin { @@ -227,7 +230,7 @@ plugins { } dependencies { - implementation("ai.liquid.leap:leap-sdk:0.10.7") + implementation("ai.liquid.leap:leap-sdk:0.10.8") } 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.8: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.8:natives@zip` +- `ai.liquid.leap:leap-sdk-linuxarm64:0.10.8:natives@zip` ## Windows native (MinGW x64) @@ -269,11 +272,11 @@ 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" + id("ai.liquid.leap.nativelibs") version "0.10.8" } dependencies { - implementation("ai.liquid.leap:leap-sdk:0.10.7") + implementation("ai.liquid.leap:leap-sdk:0.10.8") } kotlin { @@ -291,7 +294,7 @@ 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.8: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. @@ -316,7 +319,7 @@ 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", + url: "https://github.com/Liquid4All/leap-sdk/releases/download/v0.10.8/LeapSDK.xcframework.zip", checksum: "6f2721aa45d7555646f78cbcaedb57aba3d869f56b24d681ad332846e131ae3d" ) ``` @@ -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.8") + implementation("ai.liquid.leap:leap-ui:0.10.8") // 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..bdbeeba 100644 --- a/deployment/on-device/sdk/model-loading.mdx +++ b/deployment/on-device/sdk/model-loading.mdx @@ -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.8 — 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/quick-start.mdx b/deployment/on-device/sdk/quick-start.mdx index 348c902..fe67e77 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.8` 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). @@ -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.8` 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.8 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.8/LeapSDK.xcframework.zip", + checksum: "" ), .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.8/LeapModelDownloader.xcframework.zip", + checksum: "" ), .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.8/LeapOpenAIClient.xcframework.zip", + checksum: "" ), .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.8/LeapUi.xcframework.zip", + checksum: "" ), ``` @@ -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.8") + implementation("ai.liquid.leap:leap-model-downloader:0.10.8") // 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.8") // Optional: Voice assistant widget (Compose Multiplatform) - // implementation("ai.liquid.leap:leap-ui:0.10.7") + // implementation("ai.liquid.leap:leap-ui:0.10.8") } ``` @@ -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.8" [libraries] leap-sdk = { module = "ai.liquid.leap:leap-sdk", version.ref = "leapSdk" } @@ -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.8") // 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.8") + // implementation("ai.liquid.leap:leap-ui:0.10.8") // Compose for Desktop voice widget } ``` @@ -222,11 +222,11 @@ The Leap SDK is a Kotlin Multiplatform library: the same `ModelRunner` / `Conver // build.gradle.kts plugins { kotlin("multiplatform") version "2.3.20" - id("ai.liquid.leap.nativelibs") version "0.10.7" + id("ai.liquid.leap.nativelibs") version "0.10.8" } dependencies { - implementation("ai.liquid.leap:leap-sdk:0.10.7") + implementation("ai.liquid.leap:leap-sdk:0.10.8") } kotlin { @@ -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,10 @@ 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+) — SKIE bridges `Flow` with `Failure = Never`, + // so generation failures arrive here instead of on the `catch` block above. + print("Generation failed:", err.message) } } ``` @@ -489,6 +496,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/voice-assistant.mdx b/deployment/on-device/sdk/voice-assistant.mdx index 14f1884..307be74 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.8 — 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.8") ] 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.8") + implementation("ai.liquid.leap:leap-ui:0.10.8") } ``` @@ -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 From 603f037097d82408da4930be188fb24857a813a2 Mon Sep 17 00:00:00 2001 From: David Berrios Date: Wed, 27 May 2026 09:21:11 -0700 Subject: [PATCH 5/7] docs(leap-sdk): document v0.10.9 LeapDownloaderConfig.with(...) Swift fix - Add a v0.10.9 changelog entry covering the Swift-only LeapDownloaderConfig / ModelDownloader construction hang and its fix via the LeapDownloaderConfig.with(...) static factory, with the Swift call-site change and the Kotlin no-op note - Switch Swift examples from LeapDownloaderConfig(saveDir:) to LeapDownloaderConfig.with(saveDir:) across quick-start, model-loading, cloud-ai-comparison, utilities, voice-assistant, and ai-agent-usage-guide; Kotlin LeapDownloaderConfig(saveDir = ...) examples unchanged - Bump "Latest release" to v0.10.9 --- deployment/on-device/leap-sdk-changelog.mdx | 24 ++++++++++++++++++- .../on-device/sdk/ai-agent-usage-guide.mdx | 2 +- .../on-device/sdk/cloud-ai-comparison.mdx | 4 ++-- deployment/on-device/sdk/model-loading.mdx | 6 ++--- deployment/on-device/sdk/quick-start.mdx | 2 +- deployment/on-device/sdk/utilities.mdx | 2 +- deployment/on-device/sdk/voice-assistant.mdx | 2 +- 7 files changed, 32 insertions(+), 10 deletions(-) diff --git a/deployment/on-device/leap-sdk-changelog.mdx b/deployment/on-device/leap-sdk-changelog.mdx index 27ad4bb..68244f0 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.8** ([GitHub](https://github.com/Liquid4All/leap-sdk/releases/tag/v0.10.8)). +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,28 @@ val runner = downloader.loadModel( ## Per-release notes +### v0.10.9 — 2026-05-27 + +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. diff --git a/deployment/on-device/sdk/ai-agent-usage-guide.mdx b/deployment/on-device/sdk/ai-agent-usage-guide.mdx index 2337941..2015193 100644 --- a/deployment/on-device/sdk/ai-agent-usage-guide.mdx +++ b/deployment/on-device/sdk/ai-agent-usage-guide.mdx @@ -260,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? diff --git a/deployment/on-device/sdk/cloud-ai-comparison.mdx b/deployment/on-device/sdk/cloud-ai-comparison.mdx index 02030ae..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", @@ -166,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/model-loading.mdx b/deployment/on-device/sdk/model-loading.mdx index bdbeeba..1f0e999 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( diff --git a/deployment/on-device/sdk/quick-start.mdx b/deployment/on-device/sdk/quick-start.mdx index fe67e77..f56a505 100644 --- a/deployment/on-device/sdk/quick-start.mdx +++ b/deployment/on-device/sdk/quick-start.mdx @@ -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") ) diff --git a/deployment/on-device/sdk/utilities.mdx b/deployment/on-device/sdk/utilities.mdx index e57ad7c..5d9cb50 100644 --- a/deployment/on-device/sdk/utilities.mdx +++ b/deployment/on-device/sdk/utilities.mdx @@ -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 307be74..2fa494c 100644 --- a/deployment/on-device/sdk/voice-assistant.mdx +++ b/deployment/on-device/sdk/voice-assistant.mdx @@ -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() { From e9942208a7af091e73d0420405a63e90b69c8047 Mon Sep 17 00:00:00 2001 From: David Berrios Date: Wed, 3 Jun 2026 10:43:11 -0700 Subject: [PATCH 6/7] fix(leap-sdk): pin docs to v0.10.9 + real XCFramework checksums MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The v0.10.9 entry migrated Swift examples to `LeapDownloaderConfig.with(...)` but left every install pin at 0.10.8. `.with(...)` only exists in v0.10.9 (introduced in leap-android-sdk#262 / 3474b16), so the examples could not compile against the pinned version — and `LeapDownloaderConfig(saveDir:)` on 0.10.8 is the exact call that hangs. Make the install version match the code. - Bump all SPM `from:` / `.binaryTarget` URLs, Maven + Gradle coordinates, the `-jvm` classifier, the `nativelibs` plugin pins, the `:natives@zip` coordinates, the Maven ``, and the `leapSdk` catalog entry 0.10.8 -> 0.10.9 across the SDK pages and the five Android examples. Verified every artifact is published at 0.10.9 (Maven Central ``; SPM via the leap-sdk v0.10.9 tag). - Replace the four placeholder `` checksums in quick-start.mdx and the fabricated checksum in desktop-platforms.mdx (6f2721aa..., matching no real release) with the real v0.10.9 SHA-256s from leap-sdk/Package.swift. - Correct the v0.10.9 changelog heading date 2026-05-27 -> 2026-05-29 to match the published release. - Leave historical prose ("New in v0.10.8", "since v0.10.8", "v0.10.8+") unchanged. --- deployment/on-device/leap-sdk-changelog.mdx | 2 +- .../on-device/sdk/desktop-platforms.mdx | 36 ++++++++-------- deployment/on-device/sdk/openai-client.mdx | 16 +++---- deployment/on-device/sdk/quick-start.mdx | 42 +++++++++---------- deployment/on-device/sdk/voice-assistant.mdx | 8 ++-- examples/android/leap-koog-agent.mdx | 4 +- .../recipe-generator-constrained-output.mdx | 6 +-- examples/android/slogan-generator.mdx | 4 +- .../android/vision-language-model-example.mdx | 4 +- examples/android/web-content-summarizer.mdx | 4 +- 10 files changed, 63 insertions(+), 63 deletions(-) diff --git a/deployment/on-device/leap-sdk-changelog.mdx b/deployment/on-device/leap-sdk-changelog.mdx index 68244f0..d888dde 100644 --- a/deployment/on-device/leap-sdk-changelog.mdx +++ b/deployment/on-device/leap-sdk-changelog.mdx @@ -147,7 +147,7 @@ val runner = downloader.loadModel( ## Per-release notes -### v0.10.9 — 2026-05-27 +### 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. diff --git a/deployment/on-device/sdk/desktop-platforms.mdx b/deployment/on-device/sdk/desktop-platforms.mdx index 5cc0d44..44bfc57 100644 --- a/deployment/on-device/sdk/desktop-platforms.mdx +++ b/deployment/on-device/sdk/desktop-platforms.mdx @@ -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.8") + 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.8") + // 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.8") + // implementation("ai.liquid.leap:leap-ui:0.10.9") } 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.8' + 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.8 + 0.10.9 ``` @@ -197,11 +197,11 @@ dependencyResolutionManagement { // build.gradle.kts plugins { kotlin("multiplatform") version "2.3.20" - id("ai.liquid.leap.nativelibs") version "0.10.8" + id("ai.liquid.leap.nativelibs") version "0.10.9" } dependencies { - implementation("ai.liquid.leap:leap-sdk:0.10.8") + implementation("ai.liquid.leap:leap-sdk:0.10.9") } kotlin { @@ -230,7 +230,7 @@ plugins { } dependencies { - implementation("ai.liquid.leap:leap-sdk:0.10.8") + implementation("ai.liquid.leap:leap-sdk:0.10.9") } val nativesDir = layout.buildDirectory.dir("bin/linuxX64/releaseExecutable") @@ -244,7 +244,7 @@ kotlin { val leapSdkNatives by configurations.creating dependencies { - leapSdkNatives("ai.liquid.leap:leap-sdk-linuxx64:0.10.8:natives@zip") + leapSdkNatives("ai.liquid.leap:leap-sdk-linuxx64:0.10.9:natives@zip") } val installLeapNatives by tasks.registering(Copy::class) { @@ -262,8 +262,8 @@ tasks.named("linkReleaseExecutableLinuxX64") { dependsOn(installLeapNatives) } The Maven coordinates for the `-natives.zip` artifacts: -- `ai.liquid.leap:leap-sdk-linuxx64:0.10.8:natives@zip` -- `ai.liquid.leap:leap-sdk-linuxarm64:0.10.8: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) @@ -272,11 +272,11 @@ 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.8" + id("ai.liquid.leap.nativelibs") version "0.10.9" } dependencies { - implementation("ai.liquid.leap:leap-sdk:0.10.8") + implementation("ai.liquid.leap:leap-sdk:0.10.9") } kotlin { @@ -294,7 +294,7 @@ 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.8: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. @@ -319,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.8/LeapSDK.xcframework.zip", - checksum: "6f2721aa45d7555646f78cbcaedb57aba3d869f56b24d681ad332846e131ae3d" + url: "https://github.com/Liquid4All/leap-sdk/releases/download/v0.10.9/LeapSDK.xcframework.zip", + checksum: "1fdea67fe208ee56db3aba4313809b03a469e6520a49a855c212ebcfef4cea2e" ) ``` @@ -332,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.8") - implementation("ai.liquid.leap:leap-ui:0.10.8") // 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/openai-client.mdx b/deployment/on-device/sdk/openai-client.mdx index 23b61a2..425a422 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.8") + .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.8") - implementation("ai.liquid.leap:leap-openai-client:0.10.8") + 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.8") - implementation("ai.liquid.leap:leap-openai-client:0.10.8") + 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.8`. 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.8") - implementation("ai.liquid.leap:leap-openai-client:0.10.8") + implementation("ai.liquid.leap:leap-sdk:0.10.9") + implementation("ai.liquid.leap:leap-openai-client:0.10.9") } ``` diff --git a/deployment/on-device/sdk/quick-start.mdx b/deployment/on-device/sdk/quick-start.mdx index f56a505..e68fee6 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.8` +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). @@ -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.8` 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.8 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.8/LeapSDK.xcframework.zip", - checksum: "" + 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.8/LeapModelDownloader.xcframework.zip", - checksum: "" + 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.8/LeapOpenAIClient.xcframework.zip", - checksum: "" + 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.8/LeapUi.xcframework.zip", - checksum: "" + 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.8") - implementation("ai.liquid.leap:leap-model-downloader:0.10.8") // 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.8") + // implementation("ai.liquid.leap:leap-openai-client:0.10.9") // Optional: Voice assistant widget (Compose Multiplatform) - // implementation("ai.liquid.leap:leap-ui:0.10.8") + // 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.8" + leapSdk = "0.10.9" [libraries] leap-sdk = { module = "ai.liquid.leap:leap-sdk", version.ref = "leapSdk" } @@ -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.8") + implementation("ai.liquid.leap:leap-sdk:0.10.9") // Optional: - // implementation("ai.liquid.leap:leap-openai-client:0.10.8") - // implementation("ai.liquid.leap:leap-ui:0.10.8") // 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 } ``` @@ -222,11 +222,11 @@ The Leap SDK is a Kotlin Multiplatform library: the same `ModelRunner` / `Conver // build.gradle.kts plugins { kotlin("multiplatform") version "2.3.20" - id("ai.liquid.leap.nativelibs") version "0.10.8" + id("ai.liquid.leap.nativelibs") version "0.10.9" } dependencies { - implementation("ai.liquid.leap:leap-sdk:0.10.8") + implementation("ai.liquid.leap:leap-sdk:0.10.9") } kotlin { diff --git a/deployment/on-device/sdk/voice-assistant.mdx b/deployment/on-device/sdk/voice-assistant.mdx index 2fa494c..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.8 — 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.8") + .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.8") - implementation("ai.liquid.leap:leap-ui:0.10.8") + implementation("ai.liquid.leap:leap-sdk:0.10.9") + implementation("ai.liquid.leap:leap-ui:0.10.9") } ``` diff --git a/examples/android/leap-koog-agent.mdx b/examples/android/leap-koog-agent.mdx index e08bcba..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.8") - implementation("ai.liquid.leap:leap-model-downloader:0.10.8") + 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 4e93330..2422475 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.8") - implementation("ai.liquid.leap:leap-model-downloader:0.10.8") + 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") @@ -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.8 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 4be32e3..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.8") - implementation("ai.liquid.leap:leap-model-downloader:0.10.8") + 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 5189364..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.8") - implementation("ai.liquid.leap:leap-model-downloader:0.10.8") + 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 9003654..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.8") - implementation("ai.liquid.leap:leap-model-downloader:0.10.8") + 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") From 51ab0b8cd4964b27f263169cef27a65424d3cffa Mon Sep 17 00:00:00 2001 From: David Berrios Date: Wed, 3 Jun 2026 13:22:55 -0700 Subject: [PATCH 7/7] docs(leap-sdk): address PR #100 review comments - Align Kotlin plugin pins 2.3.20 -> 2.3.21 across the SDK pages and the constrained-output example, matching the SDK's build toolchain (libs.versions.toml kotlin = 2.3.21) and its own KOTLIN_NATIVE_SETUP.md. - Fix the error-model notes that claimed the SKIE bridge is "Failure = Never" and that transport failures "silently terminate the stream" with a "track whether .done was observed" workaround. Verified against SKIE 0.10.12 generated output and OpenAiClient.kt: the bridged AsyncSequence does not rethrow Kotlin exceptions; recoverable errors arrive in-band (.error only on non-2xx HTTP, malformed SSE chunks logged+skipped); transport failures are not delivered to the stream. Notes reworded to say exactly that, dropping the misleading "Failure" terminology while keeping the existing for-try-await convention. - Fix the Kotlin double-log in conversation-generation: handle the failure once in the in-band MessageResponse.Error arm; the terminal .catch {} now swallows the redundant exceptional close. - Hybrid routing example: the on-device message used LeapSDK.ChatMessage, but the example imports only LeapModelDownloader + LeapOpenAIClient. The leap-sdk types are emitted into the LeapModelDownloader module (export(project)), so LeapSDK.ChatMessage would not resolve. Use LeapModelDownloader.ChatMessage and qualify the cloud message as LeapOpenAIClient.ChatMessage.User. - model-loading: bump the LiquidInferenceEngineOptions callout version label "through v0.10.8" -> "through v0.10.9" (the claim still holds at v0.10.9). --- deployment/on-device/leap-sdk-changelog.mdx | 2 +- .../on-device/sdk/conversation-generation.mdx | 15 ++++++++------- deployment/on-device/sdk/desktop-platforms.mdx | 12 ++++++------ deployment/on-device/sdk/model-loading.mdx | 2 +- deployment/on-device/sdk/openai-client.mdx | 12 ++++++++---- deployment/on-device/sdk/quick-start.mdx | 13 +++++++------ deployment/on-device/sdk/utilities.mdx | 2 +- .../recipe-generator-constrained-output.mdx | 2 +- 8 files changed, 33 insertions(+), 27 deletions(-) diff --git a/deployment/on-device/leap-sdk-changelog.mdx b/deployment/on-device/leap-sdk-changelog.mdx index d888dde..7fc0ffe 100644 --- a/deployment/on-device/leap-sdk-changelog.mdx +++ b/deployment/on-device/leap-sdk-changelog.mdx @@ -186,7 +186,7 @@ See [OpenAI-Compatible Client](/deployment/on-device/sdk/openai-client) for the **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` with `Failure = Never`, so a thrown `LeapGenerationException` could not be observed from Swift — `for await response in conversation.generateResponse(...)` simply terminated silently. 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. +`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 { diff --git a/deployment/on-device/sdk/conversation-generation.mdx b/deployment/on-device/sdk/conversation-generation.mdx index 09a4d52..298a8cc 100644 --- a/deployment/on-device/sdk/conversation-generation.mdx +++ b/deployment/on-device/sdk/conversation-generation.mdx @@ -169,10 +169,9 @@ The async stream is the recommended way to drive generation — both platforms e print("Prompt tokens: \(stats.promptTokens), completion: \(stats.completionTokens)") } case .error(let err): - // SKIE bridges `Flow` as an `AsyncSequence` with `Failure = Never`, so the - // surrounding `for try await` cannot observe a thrown error from the Kotlin - // side. Generation failures surface as this in-band `.error` case (added in - // v0.10.8) — handle it here and present `err.message` to the user. + // 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) } } @@ -208,7 +207,9 @@ The async stream is the recommended way to drive generation — both platforms e 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() } } @@ -223,7 +224,7 @@ The async stream is the recommended way to drive generation — both platforms e } ``` - 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, which see a SKIE-bridged `AsyncSequence` with `Failure = Never`, can react), **and** as a `LeapGenerationException` thrown when the flow closes (so `Flow.catch { ... }` and `try`/`catch` keep working). Pick one path or the other — they describe the same failure. + 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. @@ -303,7 +304,7 @@ A sealed type with one case per kind of incremental output the engine emits. - **`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 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 (which see a SKIE-bridged `AsyncSequence` with `Failure = Never`) 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. +- **`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 44bfc57..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 } @@ -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' } @@ -196,7 +196,7 @@ dependencyResolutionManagement { ```kotlin // build.gradle.kts plugins { - kotlin("multiplatform") version "2.3.20" + kotlin("multiplatform") version "2.3.21" id("ai.liquid.leap.nativelibs") version "0.10.9" } @@ -226,7 +226,7 @@ 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 { @@ -271,7 +271,7 @@ The same Kotlin/Native flow works for Windows x86_64 via the MinGW-w64 toolchain ```kotlin plugins { - kotlin("multiplatform") version "2.3.20" + kotlin("multiplatform") version "2.3.21" id("ai.liquid.leap.nativelibs") version "0.10.9" } @@ -297,7 +297,7 @@ The Maven coordinates for the `-natives.zip` artifact: - `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) diff --git a/deployment/on-device/sdk/model-loading.mdx b/deployment/on-device/sdk/model-loading.mdx index 1f0e999..858a8dd 100644 --- a/deployment/on-device/sdk/model-loading.mdx +++ b/deployment/on-device/sdk/model-loading.mdx @@ -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 through v0.10.8 — 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 425a422..193176f 100644 --- a/deployment/on-device/sdk/openai-client.mdx +++ b/deployment/on-device/sdk/openai-client.mdx @@ -110,7 +110,7 @@ description: "Lightweight client for OpenAI-compatible chat completions APIs — `onEnum(of:)` gives exhaustive switching — the Swift compiler errors if a new `ChatCompletionEvent` case is added. - SKIE bridges Kotlin `Flow` to a Swift `AsyncSequence` with `Failure = Never`, so transport-level failures (network drop, TLS error, etc.) silently terminate the stream rather than throwing. Only HTTP-level errors (non-2xx response) arrive as in-band `.error` events; malformed SSE chunks are logged and skipped. If you need to react to silent termination, track whether `.done` was observed and treat a stream that ended without it as a transport failure. + 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. @@ -256,7 +256,7 @@ 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 @@ -295,16 +295,20 @@ 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 — 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: [ChatMessage.User(content: text)] + 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. - let userMessage = LeapSDK.ChatMessage(role: .user, textContent: text) + 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) } } diff --git a/deployment/on-device/sdk/quick-start.mdx b/deployment/on-device/sdk/quick-start.mdx index e68fee6..c6cb397 100644 --- a/deployment/on-device/sdk/quick-start.mdx +++ b/deployment/on-device/sdk/quick-start.mdx @@ -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+. @@ -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 } @@ -221,7 +221,7 @@ The Leap SDK is a Kotlin Multiplatform library: the same `ModelRunner` / `Conver ```kotlin // build.gradle.kts plugins { - kotlin("multiplatform") version "2.3.20" + kotlin("multiplatform") version "2.3.21" id("ai.liquid.leap.nativelibs") version "0.10.9" } @@ -477,8 +477,9 @@ Both platforms expose the same streaming shape: an async sequence of `MessageRes }.joined() print("Final:", text) case .error(let err): - // In-band failure (v0.10.8+) — SKIE bridges `Flow` with `Failure = Never`, - // so generation failures arrive here instead of on the `catch` block above. + // 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) } } diff --git a/deployment/on-device/sdk/utilities.mdx b/deployment/on-device/sdk/utilities.mdx index 5d9cb50..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 { diff --git a/examples/android/recipe-generator-constrained-output.mdx b/examples/android/recipe-generator-constrained-output.mdx index 2422475..6bde02e 100644 --- a/examples/android/recipe-generator-constrained-output.mdx +++ b/examples/android/recipe-generator-constrained-output.mdx @@ -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" } ```