From 9e13edbbc2883c0d6f5f7442b417ef1aedcc68e8 Mon Sep 17 00:00:00 2001 From: David Berrios Date: Thu, 21 May 2026 09:46:49 -0700 Subject: [PATCH 1/4] 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/4] 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/4] 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/4] 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