[Android App] ChatGPT-style chat redesign + LoRA adapter UI#414
[Android App] ChatGPT-style chat redesign + LoRA adapter UI#414sanchitmonga22 merged 16 commits intomainfrom
Conversation
The standalone LoRA demo app is no longer needed — LoRA functionality is being integrated directly into the main RunAnywhereAI example app. Also removes the module from settings.gradle.kts.
Full LoRA support across the SDK stack: C++ (runanywhere-commons): - Add supports_lora field to rac_model_info_t - Create LoRA registry (rac_lora_registry.h/cpp) with catalog storage - Add LoRA compatibility check to LLM component - Wire registry into rac_core global state - Add JNI bridge functions for all LoRA operations Kotlin SDK: - Add supportsLora to ModelInfo and registerModel() - Add LoRA public API: loadLoraAdapter, removeLoraAdapter, clearLoraAdapters, getLoadedLoraAdapters, checkLoraCompatibility - Add LoRA catalog API: registerLoraAdapter, loraAdaptersForModel, allRegisteredLoraAdapters - Add CppBridgeLoraRegistry bridge wrapper
… script Android app optimization: - Extract model registration to data/ModelList.kt (centralized, declarative) - Create data/models/AppModel.kt data class with supportsLoraAdapters field - Simplify RunAnywhereApplication to delegate to ModelList.setupModels() - Remove runBlocking calls in SDK initialization Build system: - Add build-sdk.sh wrapper script for full C++ + Kotlin build pipeline - Add Gradle tasks: buildCpp, buildFullSdk, copyNativeLibs - Replace 10 old IntelliJ run configs with 13 new ones covering the full build pipeline including C++ native builds
Redesign chat screen to clean ChatGPT-like layout (no bubbles for assistant, solid user bubbles) and add complete LoRA adapter management: picker sheet, manager screen, model badges, real adapter registration from Void2377/Qwen.
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds end-to-end LoRA adapter support across native, JNI/bridges, Kotlin/Swift SDKs, and Android UI: new C++ LoRA registry and APIs, LLM compatibility checks, JNI bindings and Kotlin/Swift bridge layers, public SDK types for LoRA, Android UI/ViewModel for adapter management, Gradle build tasks/scripts, and deletion of the RunAnyWhereLora example plus IDE/VCS metadata adjustments. Changes
Sequence Diagram(s)sequenceDiagram
participant User as Android User
participant UI as LoRA UI (Picker / Manager)
participant VM as LoraViewModel
participant SDK as RunAnywhere Kotlin SDK
participant Bridge as JNI Bridge
participant CPP as C++ LLM/Registry
User->>UI: select adapter / tap apply
UI->>VM: loadAdapter(path, scale)
VM->>SDK: checkLoraCompatibility(loraPath)
SDK->>Bridge: racLlmComponentCheckLoraCompat(handle, loraPath)
Bridge->>CPP: rac_llm_component_check_lora_compat(...)
CPP-->>Bridge: compatible / error
Bridge-->>SDK: result
SDK-->>VM: compatibility result
alt Compatible
VM->>SDK: request register/load adapter
SDK->>Bridge: racLoraRegistryRegister / load JNI calls
Bridge->>CPP: register/load actions
CPP-->>Bridge: success
Bridge-->>SDK: success
SDK-->>VM: updated adapters
VM-->>UI: update state (loaded)
else Incompatible
VM-->>UI: show error
end
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 16
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
examples/android/RunAnywhereAI/app/src/main/AndroidManifest.xml (1)
19-32:⚠️ Potential issue | 🟠 Major
android:windowSoftInputModeis an<activity>attribute and will be silently ignored on<application>.The official Android Developers documentation lists
android:windowSoftInputModeexclusively as an attribute of the<activity>element. It is not a recognized attribute on the<application>element and will be silently ignored at runtime — the keyboard will occlude the chat input field introduced in this PR.Move the attribute to the
MainActivitydeclaration:Proposed fix
<application android:name=".RunAnywhereApplication" ... - android:windowSoftInputMode="adjustResize" android:largeHeap="true" ...> ... <activity android:name=".MainActivity" android:exported="true" + android:windowSoftInputMode="adjustResize" android:theme="@style/Theme.RunAnywhereAI">For a Compose app,
adjustResizeon the activity works with Compose insets APIs. Pairandroid:windowSoftInputMode="adjustResize"withWindowCompat.setDecorFitsSystemWindows(window, false)inMainActivity.onCreateandModifier.imePadding()on the chat input container.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/android/RunAnywhereAI/app/src/main/AndroidManifest.xml` around lines 19 - 32, Remove android:windowSoftInputMode="adjustResize" from the <application> element in AndroidManifest and add android:windowSoftInputMode="adjustResize" to the <activity> declaration for MainActivity; then in MainActivity.onCreate call WindowCompat.setDecorFitsSystemWindows(window, false) and ensure the chat input Composable uses Modifier.imePadding() so the IME won’t occlude the input. Reference MainActivity and the android:windowSoftInputMode attribute when making these changes.
🧹 Nitpick comments (13)
examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/theme/AppColors.kt (1)
85-88:loraBadgeBgis missing a dark-mode variant, inconsistent with every other themeable color pair in this file.All other "light + dark" color pairs here (
userBubbleSolid/userBubbleSolidDark,messageBubbleUser/messageBubbleUserDark,backgroundGray5/backgroundGray5Dark, etc.) have an explicit dark companion and, where needed, a@Composabletheme-aware accessor.loraBadgeBghas neither.At
alpha = 0.10fover a dark surface likebackgroundPrimaryDark(0xFF0F172A), the violet tint is more visible than on the white light-mode background, so the single value may not render consistently across themes.Also note
badgePurple = primaryPurple.copy(alpha = 0.2f)already exists at line 95 —loraBadgeBgis a 10 % opacity variant of the same base color. Consider whether this deserves its own name or could be expressed asbadgePurple.copy(alpha = 0.5f)(half ofbadgePurple) for traceability.♻️ Suggested addition to bring it in line with the established pattern
val loraBadgeBg = primaryPurple.copy(alpha = 0.10f) +val loraBadgeBgDark = primaryPurple.copy(alpha = 0.15f) // Slightly stronger for dark surfaces + +@Composable +fun loraBadgeBgColor(): Color = + if (isSystemInDarkTheme()) loraBadgeBgDark else loraBadgeBg🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/theme/AppColors.kt` around lines 85 - 88, Add a dark-mode companion and theme-aware accessor for loraBadgeBg to match the file's pattern: create loraBadgeBgDark (use primaryPurple.copy(alpha = X) tuned for backgroundPrimaryDark, e.g. lower alpha than 0.10f) and expose a `@Composable` accessor (loraBadgeBg()) that returns loraBadgeBg or loraBadgeBgDark based on dark theme; consider deriving one from the existing badgePurple (e.g., badgePurple.copy(alpha = 0.5f)) to avoid duplication and pick the final alpha so the tint reads appropriately on backgroundPrimaryDark.sdk/runanywhere-commons/include/rac/infrastructure/model_management/rac_lora_registry.h (1)
28-38: Struct field documentation is inline comments — adequate but terse.The
rac_lora_entry_tstruct fields are documented with inline comments, which is fine. Just noting thatcompatible_model_idsandcompatible_model_counthave an implicit contract:compatible_model_idsis achar**array of lengthcompatible_model_count, where each element is a heap-allocatedchar*. This ownership model should be made explicit, especially for consumers implementing their own entry construction (vs. usingrac_lora_entry_copy).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-commons/include/rac/infrastructure/model_management/rac_lora_registry.h` around lines 28 - 38, The struct rac_lora_entry_t's comments don't state ownership for compatible_model_ids/compatible_model_count; update the documentation for rac_lora_entry_t (and any related API docs) to explicitly state that compatible_model_ids is a heap-allocated NULL-able char** array of length compatible_model_count and that each element is a heap-allocated char* owned by the entry (caller must free or use rac_lora_entry_copy which performs deep copy), and describe who is responsible for freeing memory and how rac_lora_entry_copy and the corresponding destructor/free function handle deep-copying and freeing these strings.examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/data/ModelList.kt (1)
146-152: Emptyurl = ""used as sentinel for multi-file models.VLM models that use multi-file registration set
url = ""because onlyfilesis used. This works becausesetupModels()checksmodel.files.isNotEmpty()before routing, but an empty string is a fragile sentinel. Consider makingurlnullable inAppModelso the intent is explicit:♻️ Suggested approach
In
AppModel.kt:data class AppModel( val id: String, val name: String, - val url: String, + val url: String? = null, val framework: InferenceFramework,Then in
setupModels(), the routing logic can usemodel.url != nullormodel.files.isNotEmpty()to decide the registration path, and single-file model registrations can assertmodel.url!!or use a safe check.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/data/ModelList.kt` around lines 146 - 152, The AppModel instances use an empty string sentinel for multi-file models; change the AppModel.url property to be nullable (String?), update all model constructors (e.g., the LFM2-VL entry) to set url = null for multi-file models instead of "", and update setupModels() to branch on model.url != null || model.files.isNotEmpty() (or prefer model.url != null for single-file registration and model.files.isNotEmpty() for multi-file) and use safe calls/assertions (model.url!!) only when url is non-null; update any downstream uses of AppModel.url to handle the nullable type accordingly.sdk/runanywhere-kotlin/src/jvmAndroidMain/kotlin/com/runanywhere/sdk/foundation/bridge/extensions/CppBridgeLoraRegistry.kt (2)
37-37:RuntimeExceptionviolates coding guidelines for error handling.The coding guidelines specify "Use sealed classes for type-safe error handling rather than generic exception handling." Throwing a bare
RuntimeExceptionhere makes it difficult for callers to handle registration failures gracefully. Consider using a sealed result type or the SDK's existingSDKErrorhierarchy, consistent with howCppBridgeLLMhandles failures.As per coding guidelines: "Use sealed classes for type-safe error handling rather than generic exception handling."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-kotlin/src/jvmAndroidMain/kotlin/com/runanywhere/sdk/foundation/bridge/extensions/CppBridgeLoraRegistry.kt` at line 37, Replace the bare RuntimeException throw in CppBridgeLoraRegistry (the registration path that currently does "throw RuntimeException(\"Failed to register LoRA adapter: $result\")") with the project's sealed error/result handling: modify the registering method in CppBridgeLoraRegistry to return the same sealed result type or SDKError used by CppBridgeLLM on failure, construct an appropriate SDKError/sealed failure case that wraps the adapter registration result/details, and propagate that sealed error to callers instead of throwing; ensure callers of the register method are updated to handle the sealed result consistently with CppBridgeLLM.
52-118: Hand-rolled JSON parsing is fragile — consider usingkotlinx.serialization.The manual regex-based JSON extraction (
extractString,extractLong,extractFloat,extractStringArray) and depth-tracking array parser don't handle escaped characters in string values (e.g.,\"inside names/descriptions). While the current catalog data is unlikely to trigger this, it's a latent bug.
kotlinx.serialization.jsonis already a project dependency. UsingJson.decodeFromStringwith a@Serializabledata class would be more robust and less code. That said, this mirrors the pattern inCppBridgeLLM.parseGenerationResult, so if there's a deliberate reason to avoid the serialization library in the bridge layer (e.g., minimizing allocations on hot paths), keep this consistent.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-kotlin/src/jvmAndroidMain/kotlin/com/runanywhere/sdk/foundation/bridge/extensions/CppBridgeLoraRegistry.kt` around lines 52 - 118, The hand-rolled, regex-based JSON parsing in parseLoraEntryJson, parseLoraEntryArrayJson and the helper methods (extractString, extractLong, extractFloat, extractStringArray) is fragile and should be replaced with kotlinx.serialization: create a `@Serializable` data class LoraEntry matching the existing fields and in parseLoraEntryJson use Json.decodeFromString<LoraEntry>(json) (catching exceptions and logging as currently done), and in parseLoraEntryArrayJson use Json.decodeFromString<List<LoraEntry>>(json) to parse arrays; remove the extract* helpers and keep existing logging/error handling around the decode calls (import kotlinx.serialization.* and kotlinx.serialization.json.Json). If there is an intentional reason to avoid kotlinx.serialization here, document that in a comment in these functions and implement proper escaping-aware parsing instead.sdk/runanywhere-kotlin/src/jvmAndroidMain/kotlin/com/runanywhere/sdk/foundation/bridge/extensions/CppBridgeModelRegistry.kt (1)
416-424:extractBooleanis subtly inconsistent with other bridge files.This implementation uses
RegexOption.IGNORE_CASEand.lowercase() == "true", whereas other bridge files (e.g.,CppBridgeVoiceAgent.kt,CppBridgeVAD.kt) use.toBooleanStrictOrNull() ?: falsewithoutIGNORE_CASE. Since JSON strictly uses lowercasetrue/false, theIGNORE_CASEflag is harmless but adds unnecessary divergence.Not blocking — the broader duplication of these JSON helpers across bridge files is a pre-existing issue — but worth noting for future consolidation.
🔧 Align with the predominant pattern
private fun extractBoolean(json: String, key: String): Boolean { val pattern = """"$key"\s*:\s*(true|false)""" - val regex = Regex(pattern, RegexOption.IGNORE_CASE) + val regex = Regex(pattern) return regex .find(json) ?.groupValues ?.get(1) - ?.lowercase() == "true" + ?.toBooleanStrictOrNull() ?: false }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-kotlin/src/jvmAndroidMain/kotlin/com/runanywhere/sdk/foundation/bridge/extensions/CppBridgeModelRegistry.kt` around lines 416 - 424, The extractBoolean function uses RegexOption.IGNORE_CASE and compares .lowercase() == "true", diverging from other bridge helpers; update extractBoolean so the regex is created without RegexOption.IGNORE_CASE and convert the captured group using .toBooleanStrictOrNull() ?: false (i.e., keep the pattern """"$key"\s*:\s*(true|false)""" and the Regex instantiation without options, then replace the .groupValues.get(1).lowercase() == "true" logic with .groupValues.get(1).toBooleanStrictOrNull() ?: false) to match the predominant pattern used in CppBridgeVoiceAgent.kt / CppBridgeVAD.kt.sdk/runanywhere-kotlin/scripts/build-kotlin.sh (1)
469-474: Dead assignment on line 469.
FLAGSis set via bash substring expansion on line 469 (${MODE:0:1}→"l"or"r") but is immediately overwritten by theif/elseon lines 470–474. The initial assignment is dead code.🧹 Remove the dead assignment
- local FLAGS="-Prunanywhere.testLocal=${MODE:0:1}" # "true" for local, "false" for remote - if [ "$MODE" = "local" ]; then + local FLAGS="" + if [ "$MODE" = "local" ]; then FLAGS="-Prunanywhere.testLocal=true"🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-kotlin/scripts/build-kotlin.sh` around lines 469 - 474, The initial dead assignment to FLAGS using substring expansion (${MODE:0:1}) should be removed and the FLAGS variable should be set only once using the existing conditional logic; update build-kotlin.sh to eliminate the redundant line that assigns FLAGS, keeping the if/else block that sets FLAGS to "-Prunanywhere.testLocal=true" when MODE == "local" and "-Prunanywhere.testLocal=false" otherwise (refer to FLAGS and MODE in the script).sdk/runanywhere-kotlin/src/commonMain/kotlin/com/runanywhere/sdk/public/extensions/RunAnywhere+ModelManagement.kt (1)
110-149: Confirm that omittingsupportsLorafromregisterMultiFileModelis intentional.Neither the
List<ModelFileDescriptor>overload (lines 110–149) nor thecompanionFilesoverload (lines 194–213) exposes asupportsLoraparameter, so any model registered through these paths will always havesupportsLora = false. If a multi-file GGUF model (e.g., a future sharded LLM) ever needs LoRA support, callers would have no way to mark it as LoRA-compatible here.If multi-file models are deliberately excluded from LoRA support (e.g., they are exclusively used for VLMs), a brief comment in the function KDoc would clarify the intent.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-kotlin/src/commonMain/kotlin/com/runanywhere/sdk/public/extensions/RunAnywhere`+ModelManagement.kt around lines 110 - 149, registerMultiFileModel currently doesn't accept a supportsLora flag so all multi-file registrations get supportsLora=false; either add an explicit Boolean parameter (e.g., supportsLora: Boolean = false) to RunAnywhere.registerMultiFileModel (and the companionFiles overload) and pass it into the created ModelInfo as supportsLora = supportsLora, or if exclusion is intentional, add a short KDoc comment on registerMultiFileModel and the companionFiles overload stating multi-file models are not LoRA-compatible to make the intent explicit.examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/MainActivity.kt (1)
78-81: MoveLog.iinto aLaunchedEffectto avoid firing on every recomposition.Calling
Log.idirectly in a composable body is an untracked side effect — it executes on every invocation ofMainAppContentwhile theReadystate holds. Wrapping it inLaunchedEffect(Unit)runs it exactly once per composition entry into this branch.♻️ Proposed fix
is SDKInitializationState.Ready -> { - Log.i("MainActivity", "App is ready to use!") + LaunchedEffect(Unit) { + Log.i("MainActivity", "App is ready to use!") + } AppNavigation() }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/MainActivity.kt` around lines 78 - 81, The Log.i call inside the SDKInitializationState.Ready branch is a side-effect running on every recomposition; move it into a Compose LaunchedEffect so it runs once when entering the Ready state. Wrap the Log.i("MainActivity", "App is ready to use!") call in LaunchedEffect(Unit) (or LaunchedEffect(state) scoped to the Ready branch) inside the same branch where AppNavigation() is invoked (refer to SDKInitializationState.Ready and Log.i) so the log is emitted only once per composition entry.build.gradle.kts (1)
146-186: New native tasks execute scripts without existence checks, unlike thesetuptask.If
scripts/build-sdk.shorscripts/build-kotlin.shis absent (e.g., on a fresh checkout where the script hasn't been committed/pulled), Gradle will throw an opaqueExecExceptionwith no actionable message. The existingsetuptask guards this with abuildScript.exists()check and prints a[WARN]instead.♻️ Suggested guard (pattern from `setup` task)
tasks.register("buildCpp") { group = "native" description = "Build C++ (runanywhere-commons) and copy .so to jniLibs" doLast { val ndkHome = resolveNdkHome(resolveAndroidHome()) + val script = file("sdk/runanywhere-kotlin/scripts/build-sdk.sh") + if (!script.exists()) { + error("[ERROR] build-sdk.sh not found at ${script.relativeTo(rootDir)}") + } exec { workingDir = file("sdk/runanywhere-kotlin") environment("ANDROID_NDK_HOME", ndkHome) commandLine("bash", "scripts/build-sdk.sh", "--cpp-only") } } }Apply the same pattern to
buildFullSdkandcopyNativeLibs.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@build.gradle.kts` around lines 146 - 186, The tasks buildCpp, buildFullSdk and copyNativeLibs call external scripts without checking the script files exist; update each task's doLast to mirror the setup task pattern: resolve the target script file (e.g., "sdk/runanywhere-kotlin/scripts/build-sdk.sh" for buildCpp/buildFullSdk and "sdk/runanywhere-kotlin/scripts/build-kotlin.sh" for copyNativeLibs), check file.exists() and log a warn and return early if missing, otherwise proceed to exec as before; reference the task names buildCpp, buildFullSdk, and copyNativeLibs when applying the guards.sdk/runanywhere-commons/src/core/rac_core.cpp (1)
313-323: Minor style inconsistency: single-lineif/returnvs multi-line in model registry.Lines 315 and 322 use
if (registry == nullptr) return …;on one line, while the equivalent model-registry functions (Lines 262–264) use multi-line blocks. Trivial, but for consistency with the rest of this file:Optional: align with existing style
rac_result_t rac_register_lora(const rac_lora_entry_t* entry) { rac_lora_registry_handle_t registry = rac_get_lora_registry(); - if (registry == nullptr) return RAC_ERROR_NOT_INITIALIZED; + if (registry == nullptr) { + return RAC_ERROR_NOT_INITIALIZED; + } return rac_lora_registry_register(registry, entry); } rac_result_t rac_get_lora_for_model(const char* model_id, rac_lora_entry_t*** out_entries, size_t* out_count) { rac_lora_registry_handle_t registry = rac_get_lora_registry(); - if (registry == nullptr) return RAC_ERROR_NOT_INITIALIZED; + if (registry == nullptr) { + return RAC_ERROR_NOT_INITIALIZED; + } return rac_lora_registry_get_for_model(registry, model_id, out_entries, out_count); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-commons/src/core/rac_core.cpp` around lines 313 - 323, The two functions rac_register_lora and rac_get_lora_for_model use single-line if-return statements; update them to match the file's multi-line style (use braces and separate return on the next line) like the model-registry functions: locate rac_register_lora and rac_get_lora_for_model, call rac_get_lora_registry() into registry, check if (registry == nullptr) with a brace-delimited block and then return RAC_ERROR_NOT_INITIALIZED inside that block, before proceeding to call rac_lora_registry_register and rac_lora_registry_get_for_model respectively.sdk/runanywhere-kotlin/src/commonMain/kotlin/com/runanywhere/sdk/public/extensions/RunAnywhere+LoRA.kt (1)
56-59: Consider a sealed class forLoraCompatibilityResult.The coding guidelines recommend sealed classes for type-safe error handling. A sealed result would eliminate the possibility of
isCompatible = truewith a non-nullerror(or vice versa):sealed class LoraCompatibilityResult { data object Compatible : LoraCompatibilityResult() data class Incompatible(val error: String) : LoraCompatibilityResult() }Not blocking — the current data class is simpler and works, but the sealed variant is more idiomatic Kotlin and prevents inconsistent states.
As per coding guidelines: "Use sealed classes for type-safe error handling rather than generic exception handling."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-kotlin/src/commonMain/kotlin/com/runanywhere/sdk/public/extensions/RunAnywhere`+LoRA.kt around lines 56 - 59, Replace the nullable-field data class LoraCompatibilityResult with a sealed class to enforce mutually exclusive states: create sealed class LoraCompatibilityResult with a singleton Compatible (or data object) and an Incompatible(data class) that carries the error string; update all places that construct or inspect LoraCompatibilityResult (factory sites, returns, and when/if checks) to produce Compatible or Incompatible(error) and use exhaustive when branches (or is checks) instead of relying on isCompatible/error null combinations.sdk/runanywhere-kotlin/scripts/build-sdk.sh (1)
34-36: Fragile help text extraction.
head -14 "$0" | tail -11hard-codes line numbers, which will silently show wrong content if the header comments are edited. Consider using a sentinel marker (e.g.,sed -n '/^# USAGE/,/^# ====/p') or embedding ausage()function instead.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-kotlin/scripts/build-sdk.sh` around lines 34 - 36, The help text extraction is fragile because it uses fixed line counts via head/tail; replace it by adding a dedicated usage() function (e.g., usage()) or by extracting between sentinel comment markers (e.g., "# USAGE" and "# ====") and call that from the --help|-h case; update the case block that currently runs head -14 "$0" | tail -11 to instead call usage() or use sed/awk to print the sentinel-delimited block so future edits to header comments won't break the help output.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/chat/ChatViewModel.kt`:
- Around line 803-813: The ChatViewModel's LoRA state becomes stale because
refreshLoraState() is private and only called during model load, and
handleLLMEvent() doesn't clear state on model unload; make refreshLoraState()
public and call it from the LoraAdapterPickerSheet onDismiss (after
LoraViewModel.loadAdapter/unloadAdapter) so hasActiveLoraAdapter is updated, and
add handling in handleLLMEvent() for model unload events to reset
currentModelSupportsLora and hasActiveLoraAdapter to false (or clear) so
indicators are not left stale; alternatively consider exposing a shared LoRA
adapter repository observed by both ChatViewModel and LoraViewModel.
In
`@examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/lora/LoraManagerScreen.kt`:
- Around line 100-104: The Text currently renders the literal "%.2f" instead of
the adapter's scale; update the Text in LoraManagerScreen.kt so the scale is
formatted with two decimals (e.g., use String.format or Kotlin's
"%.2f".format(adapter.scale)) and keep the rest of the string including the
applied/pending logic (adapter.applied) unchanged; target the Text invocation
that builds "Scale: %.2f | ${if (adapter.applied) "Applied" else "Pending"}"
and replace the %.2f part with the formatted adapter.scale value.
In
`@examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/lora/LoraViewModel.kt`:
- Around line 136-138: The checkCompatibility function performs JNI/native file
I/O and must not run on the main thread; change fun checkCompatibility(loraPath:
String): LoraCompatibilityResult to a suspend function and run the blocking call
off the UI thread (e.g., withContext(Dispatchers.IO) {
RunAnywhere.checkLoraCompatibility(loraPath) }) so callers don't ANR; update any
callers to call it from a coroutine scope. Note the native call chain:
RunAnywhere.checkLoraCompatibility → CppBridgeLLM.checkLoraCompatibility →
racLlmComponentCheckLoraCompat.
- Around line 170-189: The download loop in LoraViewModel uses
URL.openConnection() with no timeouts and a blocking read that ignores coroutine
cancellation; update the download to set read/connect timeouts on the
URLConnection and make the read loop cancellation-aware (call ensureActive()
inside the while loop) or, better, switch to OkHttp (create a Call, set
timeouts, stream the response body, and cancel the Call when the coroutine is
cancelled) so that download can be aborted via viewModelScope cancellation;
locate the code block with URL(entry.downloadUrl).openConnection(),
connection.connect(), and the while (input.read(...)) loop to apply these
changes.
- Around line 208-215: deleteAdapter currently deletes the file without ensuring
the LoRA is unloaded, risking native crashes; update deleteAdapter to first
check isLoaded(entry) and if true call unloadAdapter(entry) and await its
completion (make deleteAdapter suspend or have unloadAdapter return a
Job/Deferred you can join) before proceeding to delete the File(loraDir,
entry.filename); retain logging but only delete after successful unload and
handle failure/exception cases by logging and not removing the file.
In `@sdk/runanywhere-commons/include/rac/core/rac_core.h`:
- Around line 342-346: Add full Doxygen docs for the three new public APIs:
rac_get_lora_registry, rac_register_lora, and rac_get_lora_for_model. For each
function include `@brief`, `@param` entries describing inputs and outputs, `@return`
with all rac_result_t error codes that can be returned, and lifecycle/ownership
notes; specifically document that rac_get_lora_registry returns a pointer owned
by the library (do not free), rac_register_lora validates/copies or references
the provided rac_lora_entry (state whether caller retains ownership or must keep
it alive), and rac_get_lora_for_model allocates the out_entries array and each
rac_lora_entry* inside it and that the caller must free the array and entries
with the appropriate free function (add or reference an existing
rac_lora_entry_free or a note how to free), plus any thread-safety or vtable
lifecycle constraints relevant to these APIs.
- Around line 338-346: The LoRA API declares rac_lora_registry and uses struct
rac_lora_entry in rac_register_lora and rac_get_lora_for_model but does not
include the header that defines rac_lora_entry; add the missing include that
provides the full rac_lora_entry definition (e.g., include the header that
defines rac_lora_entry, such as rac_lora_types.h) near the top of rac_core.h
before the RAC_API declarations so callers can construct and dereference
rac_lora_entry for rac_register_lora and rac_get_lora_for_model.
In `@sdk/runanywhere-commons/include/rac/features/llm/rac_llm_component.h`:
- Around line 267-279: Update the rac_llm_component_check_lora_compat
documentation to match the implementation: state that it checks a model is
loaded, verifies adapter_path is non-empty (not that the file exists), and
confirms the backend vtable exposes load_lora, and document the caller must free
out_error with rac_free; also list all possible return error codes
(RAC_ERROR_INVALID_HANDLE, RAC_ERROR_INVALID_ARGUMENT,
RAC_ERROR_COMPONENT_NOT_READY, RAC_ERROR_NOT_SUPPORTED) and any lifecycle/vtable
requirements for the LLM component so callers know when this function is valid
to call.
In
`@sdk/runanywhere-commons/include/rac/infrastructure/model_management/rac_lora_registry.h`:
- Around line 42-67: Add per-function documentation comments to each public API
declaration in rac_lora_registry.h: for rac_lora_registry_create/destroy,
rac_lora_registry_register/remove, rac_lora_registry_get_all/get_for_model/get,
and the memory helpers
rac_lora_entry_free/rac_lora_entry_array_free/rac_lora_entry_copy. For each
function state allowed rac_result_t values (e.g., RAC_SUCCESS,
RAC_ERROR_INVALID_ARGUMENT, RAC_ERROR_NOT_FOUND, RAC_ERROR_OUT_OF_MEMORY), input
preconditions (NULL checks) and explicit ownership/lifecycle rules (e.g.,
rac_lora_registry_register deep-copies the input entry and caller retains
ownership; rac_lora_registry_get_all and rac_lora_registry_get_for_model
allocate an array that the caller must free with rac_lora_entry_array_free;
rac_lora_registry_get returns a heap-allocated entry freed with
rac_lora_entry_free). Ensure the comments are placed immediately above the
corresponding declarations and mention replacement semantics for re-registering
the same adapter id.
In `@sdk/runanywhere-commons/src/features/llm/llm_component.cpp`:
- Around line 879-889: Move the empty adapter_path validation to the very start
of the function (before calling rac_lifecycle_get_service and before acquiring
any mutex) so that if strlen(adapter_path) == 0 you set *out_error =
strdup("Empty adapter path") and return RAC_ERROR_INVALID_ARGUMENT immediately;
keep the existing rac_lifecycle_get_service/service null check (returning
RAC_ERROR_COMPONENT_NOT_READY and setting *out_error = strdup("No model
loaded")) only after the argument validation passes. Ensure you reference
adapter_path, *out_error, RAC_ERROR_INVALID_ARGUMENT, rac_lifecycle_get_service,
and RAC_ERROR_COMPONENT_NOT_READY when making this change.
In
`@sdk/runanywhere-commons/src/infrastructure/model_management/lora_registry.cpp`:
- Around line 19-21: Introduce a strong typedef/struct for adapter/model IDs
(e.g., struct RacModelId { std::string value; bool operator<(const RacModelId&)
const; bool operator==(const RacModelId&) const; };) and replace usages of
std::string keys and parameters with this type: update
rac_lora_registry::entries to std::map<RacModelId, rac_lora_entry_t*>, change
any functions that accept or compare raw string IDs to accept RacModelId (or
const RacModelId&), adjust all lookup/insert/erase/compare logic to use the new
type and provide construction from the previous string inputs where callers
originate, and ensure hash/ordering or comparator is provided if used in
hashed/ordered containers; update any API signatures that exposed raw strings so
callers construct RacModelId before calling.
In `@sdk/runanywhere-commons/src/jni/runanywhere_commons_jni.cpp`:
- Around line 1942-1959: The current loraEntryToJson function serializes
rac_lora_entry_t into a JSON string which violates the structured-types
guideline; replace this stringly-typed marshaling by returning a structured
type: either build and return a JNI object (Java LoRA entry class instance)
populated from the rac_lora_entry_t fields in loraEntryToJson, or change the API
to return a typed C/C++ struct/array (e.g., a std::vector<rac_lora_entry_t> or a
dedicated C++ LoRAEntry struct) and populate its members (id, name, description,
download_url, filename, file_size, default_scale and compatible_model_ids)
directly instead of j.dump(); ensure compatible_model_count and
compatible_model_ids are converted into an array/list field on the structured
object and update any callers to accept the new structured return type.
- Around line 1215-1253: In racLoraRegistryRegister: fix the JNI local-ref leak
by calling env->DeleteLocalRef(jModelId) after processing each element returned
by env->GetObjectArrayElement(compatibleModelIds, i), and guard the
compatible_model_ids allocation by checking the malloc/calloc return; if
allocation fails, log or handle the error, set entry.compatible_model_ids =
nullptr and entry.compatible_model_count = 0 and avoid iterating/using the array
(or free any already-duplicated strings if partially filled), so you don't
proceed with a null pointer — update the code around compatibleModelIds,
GetObjectArrayElement, and compatible_model_ids to implement these checks and
DeleteLocalRef calls.
- Around line 1277-1317: Add JNI wrappers for rac_lora_registry_remove and
rac_lora_registry_get: implement
Java_com_runanywhere_sdk_native_bridge_RunAnywhereBridge_racLoraRegistryRemove(JNIEnv*,
jclass, jstring adapterId) and
Java_com_runanywhere_sdk_native_bridge_RunAnywhereBridge_racLoraRegistryGet(JNIEnv*,
jclass, jstring adapterId). In each wrapper call rac_get_lora_registry() and
check for null (log and return failure: for remove return JNI_FALSE, for get
return env->NewStringUTF("null") or "[]"), convert the jstring adapterId via
GetStringUTFChars/ReleaseStringUTFChars, call rac_lora_registry_remove(registry,
id_str) or rac_lora_registry_get(registry, id_str, &entry), handle rac_result_t
result checking for RAC_SUCCESS, for get marshal the returned rac_lora_entry_t*
to JSON using loraEntryToJson(entry), free the entry with
rac_lora_entry_free(entry) (and for remove no entry freeing), release UTF chars,
and return appropriate JNI types (jboolean for remove, jstring JSON for get)
using env->NewStringUTF or JNI_TRUE/JNI_FALSE; ensure all error paths release
resources and return consistent values.
In `@sdk/runanywhere-kotlin/scripts/build-sdk.sh`:
- Around line 44-55: The FLAGS array is populated with --local and conditionally
--skip-build but never used; update the exec that calls build-kotlin.sh so it
passes the FLAGS array instead of hard-coding --local (i.e., call
build-kotlin.sh with "${FLAGS[@]}" followed by the passthrough args), ensuring
the SKIP_CPP / CPP_ONLY logic remains unchanged and FLAGS is the single source
of flags for the build-kotlin.sh invocation.
In
`@sdk/runanywhere-kotlin/src/jvmAndroidMain/kotlin/com/runanywhere/sdk/public/extensions/RunAnywhere`+LoRA.jvmAndroid.kt:
- Around line 100-121: The three methods registerLoraAdapter,
loraAdaptersForModel, and allRegisteredLoraAdapters call into
CppBridgeLoraRegistry without checking SDK initialization; add the same
!isInitialized guard used by other functions in this file so they throw
SDKError.notInitialized before touching JNI. Specifically, at the start of
RunAnywhere.registerLoraAdapter, RunAnywhere.loraAdaptersForModel, and
RunAnywhere.allRegisteredLoraAdapters, check if (!isInitialized) throw
SDKError.notInitialized() (or the existing pattern used elsewhere) before
delegating to CppBridgeLoraRegistry.getForModel / getAll / register.
---
Outside diff comments:
In `@examples/android/RunAnywhereAI/app/src/main/AndroidManifest.xml`:
- Around line 19-32: Remove android:windowSoftInputMode="adjustResize" from the
<application> element in AndroidManifest and add
android:windowSoftInputMode="adjustResize" to the <activity> declaration for
MainActivity; then in MainActivity.onCreate call
WindowCompat.setDecorFitsSystemWindows(window, false) and ensure the chat input
Composable uses Modifier.imePadding() so the IME won’t occlude the input.
Reference MainActivity and the android:windowSoftInputMode attribute when making
these changes.
---
Nitpick comments:
In `@build.gradle.kts`:
- Around line 146-186: The tasks buildCpp, buildFullSdk and copyNativeLibs call
external scripts without checking the script files exist; update each task's
doLast to mirror the setup task pattern: resolve the target script file (e.g.,
"sdk/runanywhere-kotlin/scripts/build-sdk.sh" for buildCpp/buildFullSdk and
"sdk/runanywhere-kotlin/scripts/build-kotlin.sh" for copyNativeLibs), check
file.exists() and log a warn and return early if missing, otherwise proceed to
exec as before; reference the task names buildCpp, buildFullSdk, and
copyNativeLibs when applying the guards.
In
`@examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/data/ModelList.kt`:
- Around line 146-152: The AppModel instances use an empty string sentinel for
multi-file models; change the AppModel.url property to be nullable (String?),
update all model constructors (e.g., the LFM2-VL entry) to set url = null for
multi-file models instead of "", and update setupModels() to branch on model.url
!= null || model.files.isNotEmpty() (or prefer model.url != null for single-file
registration and model.files.isNotEmpty() for multi-file) and use safe
calls/assertions (model.url!!) only when url is non-null; update any downstream
uses of AppModel.url to handle the nullable type accordingly.
In
`@examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/MainActivity.kt`:
- Around line 78-81: The Log.i call inside the SDKInitializationState.Ready
branch is a side-effect running on every recomposition; move it into a Compose
LaunchedEffect so it runs once when entering the Ready state. Wrap the
Log.i("MainActivity", "App is ready to use!") call in LaunchedEffect(Unit) (or
LaunchedEffect(state) scoped to the Ready branch) inside the same branch where
AppNavigation() is invoked (refer to SDKInitializationState.Ready and Log.i) so
the log is emitted only once per composition entry.
In
`@examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/theme/AppColors.kt`:
- Around line 85-88: Add a dark-mode companion and theme-aware accessor for
loraBadgeBg to match the file's pattern: create loraBadgeBgDark (use
primaryPurple.copy(alpha = X) tuned for backgroundPrimaryDark, e.g. lower alpha
than 0.10f) and expose a `@Composable` accessor (loraBadgeBg()) that returns
loraBadgeBg or loraBadgeBgDark based on dark theme; consider deriving one from
the existing badgePurple (e.g., badgePurple.copy(alpha = 0.5f)) to avoid
duplication and pick the final alpha so the tint reads appropriately on
backgroundPrimaryDark.
In
`@sdk/runanywhere-commons/include/rac/infrastructure/model_management/rac_lora_registry.h`:
- Around line 28-38: The struct rac_lora_entry_t's comments don't state
ownership for compatible_model_ids/compatible_model_count; update the
documentation for rac_lora_entry_t (and any related API docs) to explicitly
state that compatible_model_ids is a heap-allocated NULL-able char** array of
length compatible_model_count and that each element is a heap-allocated char*
owned by the entry (caller must free or use rac_lora_entry_copy which performs
deep copy), and describe who is responsible for freeing memory and how
rac_lora_entry_copy and the corresponding destructor/free function handle
deep-copying and freeing these strings.
In `@sdk/runanywhere-commons/src/core/rac_core.cpp`:
- Around line 313-323: The two functions rac_register_lora and
rac_get_lora_for_model use single-line if-return statements; update them to
match the file's multi-line style (use braces and separate return on the next
line) like the model-registry functions: locate rac_register_lora and
rac_get_lora_for_model, call rac_get_lora_registry() into registry, check if
(registry == nullptr) with a brace-delimited block and then return
RAC_ERROR_NOT_INITIALIZED inside that block, before proceeding to call
rac_lora_registry_register and rac_lora_registry_get_for_model respectively.
In `@sdk/runanywhere-kotlin/scripts/build-kotlin.sh`:
- Around line 469-474: The initial dead assignment to FLAGS using substring
expansion (${MODE:0:1}) should be removed and the FLAGS variable should be set
only once using the existing conditional logic; update build-kotlin.sh to
eliminate the redundant line that assigns FLAGS, keeping the if/else block that
sets FLAGS to "-Prunanywhere.testLocal=true" when MODE == "local" and
"-Prunanywhere.testLocal=false" otherwise (refer to FLAGS and MODE in the
script).
In `@sdk/runanywhere-kotlin/scripts/build-sdk.sh`:
- Around line 34-36: The help text extraction is fragile because it uses fixed
line counts via head/tail; replace it by adding a dedicated usage() function
(e.g., usage()) or by extracting between sentinel comment markers (e.g., "#
USAGE" and "# ====") and call that from the --help|-h case; update the case
block that currently runs head -14 "$0" | tail -11 to instead call usage() or
use sed/awk to print the sentinel-delimited block so future edits to header
comments won't break the help output.
In
`@sdk/runanywhere-kotlin/src/commonMain/kotlin/com/runanywhere/sdk/public/extensions/RunAnywhere`+LoRA.kt:
- Around line 56-59: Replace the nullable-field data class
LoraCompatibilityResult with a sealed class to enforce mutually exclusive
states: create sealed class LoraCompatibilityResult with a singleton Compatible
(or data object) and an Incompatible(data class) that carries the error string;
update all places that construct or inspect LoraCompatibilityResult (factory
sites, returns, and when/if checks) to produce Compatible or Incompatible(error)
and use exhaustive when branches (or is checks) instead of relying on
isCompatible/error null combinations.
In
`@sdk/runanywhere-kotlin/src/commonMain/kotlin/com/runanywhere/sdk/public/extensions/RunAnywhere`+ModelManagement.kt:
- Around line 110-149: registerMultiFileModel currently doesn't accept a
supportsLora flag so all multi-file registrations get supportsLora=false; either
add an explicit Boolean parameter (e.g., supportsLora: Boolean = false) to
RunAnywhere.registerMultiFileModel (and the companionFiles overload) and pass it
into the created ModelInfo as supportsLora = supportsLora, or if exclusion is
intentional, add a short KDoc comment on registerMultiFileModel and the
companionFiles overload stating multi-file models are not LoRA-compatible to
make the intent explicit.
In
`@sdk/runanywhere-kotlin/src/jvmAndroidMain/kotlin/com/runanywhere/sdk/foundation/bridge/extensions/CppBridgeLoraRegistry.kt`:
- Line 37: Replace the bare RuntimeException throw in CppBridgeLoraRegistry (the
registration path that currently does "throw RuntimeException(\"Failed to
register LoRA adapter: $result\")") with the project's sealed error/result
handling: modify the registering method in CppBridgeLoraRegistry to return the
same sealed result type or SDKError used by CppBridgeLLM on failure, construct
an appropriate SDKError/sealed failure case that wraps the adapter registration
result/details, and propagate that sealed error to callers instead of throwing;
ensure callers of the register method are updated to handle the sealed result
consistently with CppBridgeLLM.
- Around line 52-118: The hand-rolled, regex-based JSON parsing in
parseLoraEntryJson, parseLoraEntryArrayJson and the helper methods
(extractString, extractLong, extractFloat, extractStringArray) is fragile and
should be replaced with kotlinx.serialization: create a `@Serializable` data class
LoraEntry matching the existing fields and in parseLoraEntryJson use
Json.decodeFromString<LoraEntry>(json) (catching exceptions and logging as
currently done), and in parseLoraEntryArrayJson use
Json.decodeFromString<List<LoraEntry>>(json) to parse arrays; remove the
extract* helpers and keep existing logging/error handling around the decode
calls (import kotlinx.serialization.* and kotlinx.serialization.json.Json). If
there is an intentional reason to avoid kotlinx.serialization here, document
that in a comment in these functions and implement proper escaping-aware parsing
instead.
In
`@sdk/runanywhere-kotlin/src/jvmAndroidMain/kotlin/com/runanywhere/sdk/foundation/bridge/extensions/CppBridgeModelRegistry.kt`:
- Around line 416-424: The extractBoolean function uses RegexOption.IGNORE_CASE
and compares .lowercase() == "true", diverging from other bridge helpers; update
extractBoolean so the regex is created without RegexOption.IGNORE_CASE and
convert the captured group using .toBooleanStrictOrNull() ?: false (i.e., keep
the pattern """"$key"\s*:\s*(true|false)""" and the Regex instantiation without
options, then replace the .groupValues.get(1).lowercase() == "true" logic with
.groupValues.get(1).toBooleanStrictOrNull() ?: false) to match the predominant
pattern used in CppBridgeVoiceAgent.kt / CppBridgeVAD.kt.
...ereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/lora/LoraManagerScreen.kt
Show resolved
Hide resolved
...nywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/lora/LoraViewModel.kt
Outdated
Show resolved
Hide resolved
| withContext(Dispatchers.IO) { | ||
| val connection = URL(entry.downloadUrl).openConnection() | ||
| connection.connect() | ||
| val totalSize = connection.contentLengthLong.takeIf { it > 0 } ?: entry.fileSize | ||
| var downloaded = 0L | ||
|
|
||
| connection.getInputStream().buffered().use { input -> | ||
| destFile.outputStream().buffered().use { output -> | ||
| val buffer = ByteArray(8192) | ||
| var bytesRead: Int | ||
| while (input.read(buffer).also { bytesRead = it } != -1) { | ||
| output.write(buffer, 0, bytesRead) | ||
| downloaded += bytesRead | ||
| if (totalSize > 0) { | ||
| val progress = (downloaded.toFloat() / totalSize).coerceIn(0f, 1f) | ||
| _uiState.value = _uiState.value.copy(downloadProgress = progress) | ||
| } | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Download connection has no timeouts and no cancellation support.
URL.openConnection() uses platform-default timeouts, which can be very long or effectively infinite on some Android versions. Additionally, the blocking I/O loop isn't cancellation-aware — if viewModelScope is cancelled, the coroutine is marked cancelled but the blocking read continues until the stream ends.
🛡️ Suggested improvements
withContext(Dispatchers.IO) {
- val connection = URL(entry.downloadUrl).openConnection()
+ val connection = URL(entry.downloadUrl).openConnection().apply {
+ connectTimeout = 15_000
+ readTimeout = 30_000
+ }
connection.connect()For cancellation, consider using OkHttp (already a project dependency) which supports Call.cancel(), or wrap the read loop with ensureActive() checks:
while (input.read(buffer).also { bytesRead = it } != -1) {
ensureActive() // throws CancellationException if scope cancelled
output.write(buffer, 0, bytesRead)
// ...
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| withContext(Dispatchers.IO) { | |
| val connection = URL(entry.downloadUrl).openConnection() | |
| connection.connect() | |
| val totalSize = connection.contentLengthLong.takeIf { it > 0 } ?: entry.fileSize | |
| var downloaded = 0L | |
| connection.getInputStream().buffered().use { input -> | |
| destFile.outputStream().buffered().use { output -> | |
| val buffer = ByteArray(8192) | |
| var bytesRead: Int | |
| while (input.read(buffer).also { bytesRead = it } != -1) { | |
| output.write(buffer, 0, bytesRead) | |
| downloaded += bytesRead | |
| if (totalSize > 0) { | |
| val progress = (downloaded.toFloat() / totalSize).coerceIn(0f, 1f) | |
| _uiState.value = _uiState.value.copy(downloadProgress = progress) | |
| } | |
| } | |
| } | |
| } | |
| withContext(Dispatchers.IO) { | |
| val connection = URL(entry.downloadUrl).openConnection().apply { | |
| connectTimeout = 15_000 | |
| readTimeout = 30_000 | |
| } | |
| connection.connect() | |
| val totalSize = connection.contentLengthLong.takeIf { it > 0 } ?: entry.fileSize | |
| var downloaded = 0L | |
| connection.getInputStream().buffered().use { input -> | |
| destFile.outputStream().buffered().use { output -> | |
| val buffer = ByteArray(8192) | |
| var bytesRead: Int | |
| while (input.read(buffer).also { bytesRead = it } != -1) { | |
| output.write(buffer, 0, bytesRead) | |
| downloaded += bytesRead | |
| if (totalSize > 0) { | |
| val progress = (downloaded.toFloat() / totalSize).coerceIn(0f, 1f) | |
| _uiState.value = _uiState.value.copy(downloadProgress = progress) | |
| } | |
| } | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/lora/LoraViewModel.kt`
around lines 170 - 189, The download loop in LoraViewModel uses
URL.openConnection() with no timeouts and a blocking read that ignores coroutine
cancellation; update the download to set read/connect timeouts on the
URLConnection and make the read loop cancellation-aware (call ensureActive()
inside the while loop) or, better, switch to OkHttp (create a Call, set
timeouts, stream the response body, and cancel the Call when the coroutine is
cancelled) so that download can be aborted via viewModelScope cancellation;
locate the code block with URL(entry.downloadUrl).openConnection(),
connection.connect(), and the while (input.read(...)) loop to apply these
changes.
...nywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/lora/LoraViewModel.kt
Outdated
Show resolved
Hide resolved
| JNIEXPORT jstring JNICALL | ||
| Java_com_runanywhere_sdk_native_bridge_RunAnywhereBridge_racLoraRegistryGetForModel( | ||
| JNIEnv* env, jclass clazz, jstring modelId) { | ||
| if (!modelId) return env->NewStringUTF("[]"); | ||
| const char* id_str = env->GetStringUTFChars(modelId, nullptr); | ||
| rac_lora_entry_t** entries = nullptr; | ||
| size_t count = 0; | ||
| rac_result_t result = rac_get_lora_for_model(id_str, &entries, &count); | ||
| env->ReleaseStringUTFChars(modelId, id_str); | ||
| if (result != RAC_SUCCESS || !entries || count == 0) return env->NewStringUTF("[]"); | ||
| std::string json = "["; | ||
| for (size_t i = 0; i < count; i++) { | ||
| if (i > 0) json += ","; | ||
| json += loraEntryToJson(entries[i]); | ||
| } | ||
| json += "]"; | ||
| rac_lora_entry_array_free(entries, count); | ||
| return env->NewStringUTF(json.c_str()); | ||
| } | ||
|
|
||
| JNIEXPORT jstring JNICALL | ||
| Java_com_runanywhere_sdk_native_bridge_RunAnywhereBridge_racLoraRegistryGetAll( | ||
| JNIEnv* env, jclass clazz) { | ||
| rac_lora_registry_handle_t registry = rac_get_lora_registry(); | ||
| if (!registry) { | ||
| LOGe("LoRA registry not initialized"); | ||
| return env->NewStringUTF("[]"); | ||
| } | ||
| rac_lora_entry_t** entries = nullptr; | ||
| size_t count = 0; | ||
| rac_result_t result = rac_lora_registry_get_all(registry, &entries, &count); | ||
| if (result != RAC_SUCCESS || !entries || count == 0) return env->NewStringUTF("[]"); | ||
| std::string json = "["; | ||
| for (size_t i = 0; i < count; i++) { | ||
| if (i > 0) json += ","; | ||
| json += loraEntryToJson(entries[i]); | ||
| } | ||
| json += "]"; | ||
| rac_lora_entry_array_free(entries, count); | ||
| return env->NewStringUTF(json.c_str()); | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# Find LoRA registry C API header files to understand the complete interface
fd -t f "\.h$" "runanywhere-commons" | xargs rg -l "rac_lora_registry"Repository: RunanywhereAI/runanywhere-sdks
Length of output: 625
🏁 Script executed:
# List all LoRA registry C API functions defined in headers
rg -nP "rac_lora_registry_\w+\s*\(" sdk/runanywhere-commons/ --type=hRepository: RunanywhereAI/runanywhere-sdks
Length of output: 1284
🏁 Script executed:
# List all JNI methods for LoRA in the file
rg -nP "Java_com_runanywhere_sdk_native_bridge_RunAnywhereBridge_racLora\w+" sdk/runanywhere-commons/src/jni/runanywhere_commons_jni.cppRepository: RunanywhereAI/runanywhere-sdks
Length of output: 335
🏁 Script executed:
# Check for any remove/delete functions in C API
rg -nP "rac_lora.*remove|rac_lora.*delete" sdk/runanywhere-commons/ --type=hRepository: RunanywhereAI/runanywhere-sdks
Length of output: 245
🏁 Script executed:
# Get full signatures of the missing C API functions
sed -n '49p;60p' sdk/runanywhere-commons/include/rac/infrastructure/model_management/rac_lora_registry.hRepository: RunanywhereAI/runanywhere-sdks
Length of output: 232
🏁 Script executed:
# Check Kotlin bindings for LoRA registry (especially remove and get)
rg -nP "racLoraRegistry(Remove|Get)\b" sdk/runanywhere-kotlin/ --type=ktRepository: RunanywhereAI/runanywhere-sdks
Length of output: 100
🏁 Script executed:
# Check Kotlin bindings for LoRA registry (especially remove and get)
rg -nP "racLoraRegistry(Remove|Get)" sdk/runanywhere-kotlin/Repository: RunanywhereAI/runanywhere-sdks
Length of output: 793
🏁 Script executed:
# Get the complete function signatures for both missing functions
rg -A1 "rac_lora_registry_remove\|rac_lora_registry_get" sdk/runanywhere-commons/include/rac/infrastructure/model_management/rac_lora_registry.hRepository: RunanywhereAI/runanywhere-sdks
Length of output: 56
🏁 Script executed:
# Get full signatures for the two missing functions
cat sdk/runanywhere-commons/include/rac/infrastructure/model_management/rac_lora_registry.h | grep -A2 "rac_lora_registry_remove\|rac_lora_registry_get[^_]"Repository: RunanywhereAI/runanywhere-sdks
Length of output: 451
🏁 Script executed:
# Check if remove and get are mentioned anywhere in Kotlin bridge files
rg "racLoraRegistryRemove|racLoraRegistryGet[^F]" sdk/runanywhere-kotlin/Repository: RunanywhereAI/runanywhere-sdks
Length of output: 410
🏁 Script executed:
# Check the full content of the LoRA registry header to understand all functions
cat sdk/runanywhere-commons/include/rac/infrastructure/model_management/rac_lora_registry.hRepository: RunanywhereAI/runanywhere-sdks
Length of output: 3160
Implement missing JNI wrappers and Kotlin bindings for rac_lora_registry_remove and rac_lora_registry_get.
The C API defines six public functions (register, remove, get_all, get_for_model, get, create/destroy), but the JNI layer only wraps three (register, get_all, get_for_model). Missing are rac_lora_registry_remove(handle, adapter_id) and rac_lora_registry_get(handle, adapter_id, out_entry), which violates the guideline requiring JNI to marshal all C API functions.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@sdk/runanywhere-commons/src/jni/runanywhere_commons_jni.cpp` around lines
1277 - 1317, Add JNI wrappers for rac_lora_registry_remove and
rac_lora_registry_get: implement
Java_com_runanywhere_sdk_native_bridge_RunAnywhereBridge_racLoraRegistryRemove(JNIEnv*,
jclass, jstring adapterId) and
Java_com_runanywhere_sdk_native_bridge_RunAnywhereBridge_racLoraRegistryGet(JNIEnv*,
jclass, jstring adapterId). In each wrapper call rac_get_lora_registry() and
check for null (log and return failure: for remove return JNI_FALSE, for get
return env->NewStringUTF("null") or "[]"), convert the jstring adapterId via
GetStringUTFChars/ReleaseStringUTFChars, call rac_lora_registry_remove(registry,
id_str) or rac_lora_registry_get(registry, id_str, &entry), handle rac_result_t
result checking for RAC_SUCCESS, for get marshal the returned rac_lora_entry_t*
to JSON using loraEntryToJson(entry), free the entry with
rac_lora_entry_free(entry) (and for remove no entry freeing), release UTF chars,
and return appropriate JNI types (jboolean for remove, jstring JSON for get)
using env->NewStringUTF or JNI_TRUE/JNI_FALSE; ensure all error paths release
resources and return consistent values.
| static std::string loraEntryToJson(const rac_lora_entry_t* entry) { | ||
| if (!entry) return "null"; | ||
| nlohmann::json j; | ||
| j["id"] = entry->id ? entry->id : ""; | ||
| j["name"] = entry->name ? entry->name : ""; | ||
| j["description"] = entry->description ? entry->description : ""; | ||
| j["download_url"] = entry->download_url ? entry->download_url : ""; | ||
| j["filename"] = entry->filename ? entry->filename : ""; | ||
| j["file_size"] = entry->file_size; | ||
| j["default_scale"] = entry->default_scale; | ||
| nlohmann::json ids = nlohmann::json::array(); | ||
| for (size_t i = 0; i < entry->compatible_model_count; ++i) { | ||
| if (entry->compatible_model_ids[i]) | ||
| ids.push_back(entry->compatible_model_ids[i]); | ||
| } | ||
| j["compatible_model_ids"] = ids; | ||
| return j.dump(); | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Avoid stringly‑typed JSON marshaling for LoRA entries.
The LoRA registry additions serialize entries as JSON strings; this contradicts the structured‑types guideline. Consider returning structured JNI objects (or a typed C struct array) rather than JSON strings for adapter data.
As per coding guidelines, "Always use structured types, never use strings directly for consistency and scalability".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@sdk/runanywhere-commons/src/jni/runanywhere_commons_jni.cpp` around lines
1942 - 1959, The current loraEntryToJson function serializes rac_lora_entry_t
into a JSON string which violates the structured-types guideline; replace this
stringly-typed marshaling by returning a structured type: either build and
return a JNI object (Java LoRA entry class instance) populated from the
rac_lora_entry_t fields in loraEntryToJson, or change the API to return a typed
C/C++ struct/array (e.g., a std::vector<rac_lora_entry_t> or a dedicated C++
LoRAEntry struct) and populate its members (id, name, description, download_url,
filename, file_size, default_scale and compatible_model_ids) directly instead of
j.dump(); ensure compatible_model_count and compatible_model_ids are converted
into an array/list field on the structured object and update any callers to
accept the new structured return type.
| # Build flags for build-kotlin.sh | ||
| FLAGS=(--local) | ||
|
|
||
| if [ "$SKIP_CPP" = true ]; then | ||
| # Just copy .so files and build Kotlin | ||
| FLAGS+=(--skip-build) | ||
| if [ "$CPP_ONLY" = true ]; then | ||
| echo "Cannot use --skip-cpp and --cpp-only together" | ||
| exit 1 | ||
| fi | ||
| # Run copy + Kotlin build | ||
| exec "${SCRIPT_DIR}/build-kotlin.sh" --local "${PASSTHROUGH_ARGS[@]}" |
There was a problem hiding this comment.
Bug: FLAGS array is built but never used — --skip-cpp doesn't actually skip the C++ build.
The FLAGS array accumulates --skip-build on Line 49 when --skip-cpp is set, but the exec call on Line 55 hard-codes --local and never references ${FLAGS[@]}. This means running ./build-sdk.sh --skip-cpp will invoke build-kotlin.sh --local without --skip-build, defeating the purpose.
Proposed fix
# Build flags for build-kotlin.sh
FLAGS=(--local)
if [ "$SKIP_CPP" = true ]; then
# Just copy .so files and build Kotlin
FLAGS+=(--skip-build)
if [ "$CPP_ONLY" = true ]; then
echo "Cannot use --skip-cpp and --cpp-only together"
exit 1
fi
# Run copy + Kotlin build
- exec "${SCRIPT_DIR}/build-kotlin.sh" --local "${PASSTHROUGH_ARGS[@]}"
+ exec "${SCRIPT_DIR}/build-kotlin.sh" "${FLAGS[@]}" "${PASSTHROUGH_ARGS[@]}"
elif [ "$CPP_ONLY" = true ]; then
# Build C++ + copy, skip Kotlin Gradle build
exec "${SCRIPT_DIR}/build-kotlin.sh" --local --rebuild-commons --skip-build "${PASSTHROUGH_ARGS[@]}"
else
# Full pipeline: C++ + copy + Kotlin
exec "${SCRIPT_DIR}/build-kotlin.sh" --local --rebuild-commons "${PASSTHROUGH_ARGS[@]}"
fi📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| # Build flags for build-kotlin.sh | |
| FLAGS=(--local) | |
| if [ "$SKIP_CPP" = true ]; then | |
| # Just copy .so files and build Kotlin | |
| FLAGS+=(--skip-build) | |
| if [ "$CPP_ONLY" = true ]; then | |
| echo "Cannot use --skip-cpp and --cpp-only together" | |
| exit 1 | |
| fi | |
| # Run copy + Kotlin build | |
| exec "${SCRIPT_DIR}/build-kotlin.sh" --local "${PASSTHROUGH_ARGS[@]}" | |
| # Build flags for build-kotlin.sh | |
| FLAGS=(--local) | |
| if [ "$SKIP_CPP" = true ]; then | |
| # Just copy .so files and build Kotlin | |
| FLAGS+=(--skip-build) | |
| if [ "$CPP_ONLY" = true ]; then | |
| echo "Cannot use --skip-cpp and --cpp-only together" | |
| exit 1 | |
| fi | |
| # Run copy + Kotlin build | |
| exec "${SCRIPT_DIR}/build-kotlin.sh" "${FLAGS[@]}" "${PASSTHROUGH_ARGS[@]}" |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@sdk/runanywhere-kotlin/scripts/build-sdk.sh` around lines 44 - 55, The FLAGS
array is populated with --local and conditionally --skip-build but never used;
update the exec that calls build-kotlin.sh so it passes the FLAGS array instead
of hard-coding --local (i.e., call build-kotlin.sh with "${FLAGS[@]}" followed
by the passthrough args), ensuring the SKIP_CPP / CPP_ONLY logic remains
unchanged and FLAGS is the single source of flags for the build-kotlin.sh
invocation.
...c/jvmAndroidMain/kotlin/com/runanywhere/sdk/public/extensions/RunAnywhere+LoRA.jvmAndroid.kt
Show resolved
Hide resolved
- Move windowSoftInputMode from <application> to <activity> (was silently ignored) - Fix %.2f literal string in LoraManagerScreen (now uses .format()) - Make ChatViewModel.refreshLoraState() public, call on picker dismiss - Add download timeouts (30s connect, 60s read), cancellation, atomic temp file - Unload adapter before deleting file to prevent native crash - Make checkCompatibility async (off main thread) - Fix JNI local-ref leak for jModelId in lora registry register - Add malloc failure check for compatible_model_ids array - Fix build-sdk.sh --skip-cpp flag not working (FLAGS array was unused)
d0da302 to
00c73b4
Compare
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 13
🧹 Nitpick comments (12)
examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/theme/Dimensions.kt (2)
109-110:assistantIconSize(20.dp) is defined inline in the chat section rather than in the establishedICON SIZESsection;assistantIconSpacingduplicatesmedium.
- 20.dp doesn't correspond to any named icon size constant (existing values: 8, 18, 28, 48, 60, 72, 80 dp). If this size is chat-specific and unlikely to be reused, keeping it here is fine; otherwise consider adding it to
ICON SIZES.assistantIconSpacing = 10.dpis identical tomedium. Usingmediumdirectly, or aliasing it (val assistantIconSpacing = medium), avoids a silent duplicate.♻️ Proposed refactor
- val assistantIconSpacing = 10.dp + val assistantIconSpacing = medium // 10.dp🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/theme/Dimensions.kt` around lines 109 - 110, assistantIconSize (20.dp) is declared inline in the chat section and assistantIconSpacing duplicates the existing medium constant; move or align these with the ICON SIZES group: either add a named 20.dp constant to the ICON SIZES block (if reuse is expected) and replace inline assistantIconSize with that constant, or leave assistantIconSize inline if chat-only, and replace assistantIconSpacing with the existing medium constant (e.g., val assistantIconSpacing = medium) to avoid a silent duplicate; update usages of assistantIconSize and assistantIconSpacing accordingly.
113-113:userBubbleCornerRadiusis an alias of the already-existingmessageBubbleCornerRadius.Both resolve to
cornerRadiusBubble(18.dp). If the intent is for them to be independently tunable in the future, this is fine; but until they diverge it is unnecessary duplication. Consider simply reusingmessageBubbleCornerRadiusat call sites, or at minimum add a comment explaining why a separate alias is needed.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/theme/Dimensions.kt` at line 113, userBubbleCornerRadius is a redundant alias of messageBubbleCornerRadius (both set to cornerRadiusBubble); either remove userBubbleCornerRadius and reference messageBubbleCornerRadius directly at call sites, or keep userBubbleCornerRadius but add a clear comment above it explaining it intentionally aliases messageBubbleCornerRadius for future independent tuning—refer to the symbols userBubbleCornerRadius, messageBubbleCornerRadius, and cornerRadiusBubble to locate and update the declaration.build.gradle.kts (3)
160-162:buildFullSdkis grouped under"native"but also builds the Kotlin SDK.Its description is "Full pipeline: build C++ + copy .so + build Kotlin SDK", which straddles both the
"native"and"sdk"groups. A user running./gradlew tasks --group sdkwon't find it. Consider grouping it under"sdk"(more complete scope) or a dedicated"pipeline"group to aid discoverability.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@build.gradle.kts` around lines 160 - 162, The task buildFullSdk is currently placed in group "native" but also builds the Kotlin SDK; change its group assignment to a more appropriate scope (e.g., "sdk" or a new "pipeline" group) so it appears when users list sdk tasks; locate the tasks.register("buildFullSdk") block and replace group = "native" with group = "sdk" (or group = "pipeline") and update the description if you create a new group to keep semantics clear.
146-186: Consider adding script-existence guards (likesetupdoes) to avoid cryptic failures.The
setuptask guards itsexecwithif (buildScript.exists())and prints a clear warning on miss. These three new tasks will throw a raw Gradle process exception if the scripts are absent, which is harder to diagnose. A lightweight guard would match the existing pattern.♻️ Suggested pattern (same for all three tasks)
tasks.register("buildCpp") { group = "native" description = "Build C++ (runanywhere-commons) and copy .so to jniLibs" doLast { + val script = file("sdk/runanywhere-kotlin/scripts/build-sdk.sh") + check(script.exists()) { "build-sdk.sh not found at ${script.relativeTo(rootDir)}" } val ndkHome = resolveNdkHome(resolveAndroidHome()) exec { workingDir = file("sdk/runanywhere-kotlin") environment("ANDROID_NDK_HOME", ndkHome) commandLine("bash", "scripts/build-sdk.sh", "--cpp-only") } } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@build.gradle.kts` around lines 146 - 186, The three tasks buildCpp, buildFullSdk, and copyNativeLibs currently call scripts directly and will throw a raw exec exception if the target scripts are missing; add the same existence guard used by the setup task: before calling exec, locate the script file in the workingDir (e.g., "sdk/runanywhere-kotlin/scripts/build-sdk.sh" or "scripts/build-kotlin.sh"), check file.exists(), and if not print a clear warning via logger/println and return from doLast so the task no-ops instead of failing; keep existing environment setup (resolveNdkHome/resolveAndroidHome) and exec invocation otherwise.
174-185:ANDROID_NDK_HOMEis redundant in a copy-only task.
copyNativeLibsis described as "no C++ rebuild" and delegates tobuild-kotlin.sh --local --skip-build, which should not require the NDK. SettingANDROID_NDK_HOMEhere creates a dependency on NDK availability for a task that doesn't compile anything.♻️ Suggested simplification
tasks.register("copyNativeLibs") { group = "native" description = "Copy .so from dist/ to jniLibs/ (no C++ rebuild)" doLast { - val ndkHome = resolveNdkHome(resolveAndroidHome()) exec { workingDir = file("sdk/runanywhere-kotlin") - environment("ANDROID_NDK_HOME", ndkHome) commandLine("bash", "scripts/build-kotlin.sh", "--local", "--skip-build") } } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@build.gradle.kts` around lines 174 - 185, The copyNativeLibs task currently injects ANDROID_NDK_HOME even though it runs build-kotlin.sh with --skip-build; remove the environment("ANDROID_NDK_HOME", ndkHome) line from the exec block in the tasks.register("copyNativeLibs") configuration so the copy-only task does not depend on the NDK; if other tasks or scripts still need ANDROID_NDK_HOME, set it only in the tasks that perform native builds or in the build script paths that call resolveNdkHome/resolveAndroidHome instead.sdk/runanywhere-kotlin/src/jvmAndroidMain/kotlin/com/runanywhere/sdk/foundation/bridge/extensions/CppBridgeLoraRegistry.kt (1)
92-129:extractString,extractLong,extractFloat,LogLevel, andlogare all duplicated fromCppBridgeModelRegistry.kt.These utilities are nearly identical to the equivalents at
CppBridgeModelRegistry.ktlines 385–440. Consider extracting them into a sharedCppBridgeJsonUtils(or similar) object injvmAndroidMainand having both bridge files delegate to it.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-kotlin/src/jvmAndroidMain/kotlin/com/runanywhere/sdk/foundation/bridge/extensions/CppBridgeLoraRegistry.kt` around lines 92 - 129, Multiple JSON parsing helpers (extractString, extractLong, extractFloat, extractStringArray) and logging utilities (LogLevel enum and log function) are duplicated from CppBridgeModelRegistry; extract them into a single shared utility (e.g., CppBridgeJsonUtils) in jvmAndroidMain exposing those functions and a log wrapper, then replace the local implementations in CppBridgeLoraRegistry.kt and CppBridgeModelRegistry.kt to call the shared methods (use the same function names: extractString, extractLong, extractFloat, extractStringArray and the LogLevel/log entrypoint so callers need only import and delegate to the new shared object).sdk/runanywhere-kotlin/src/jvmAndroidMain/kotlin/com/runanywhere/sdk/public/extensions/RunAnywhere+LoRA.jvmAndroid.kt (1)
100-113:RuntimeExceptionfromCppBridgeLoraRegistry.registerpropagates uncaught through the public API.
CppBridgeLoraRegistry.registerthrowsRuntimeExceptionon failure.registerLoraAdapterlets it escape unchanged, making the public API inconsistent — all other LoRA functions (loadLoraAdapter,removeLoraAdapter,clearLoraAdapters) throwSDKError.llm.♻️ Proposed fix
actual fun RunAnywhere.registerLoraAdapter(entry: LoraAdapterCatalogEntry) { + if (!isInitialized) throw SDKError.notInitialized("SDK not initialized") try { CppBridgeLoraRegistry.register( CppBridgeLoraRegistry.LoraEntry( id = entry.id, name = entry.name, description = entry.description, downloadUrl = entry.downloadUrl, filename = entry.filename, compatibleModelIds = entry.compatibleModelIds, fileSize = entry.fileSize, defaultScale = entry.defaultScale, ), ) + } catch (e: RuntimeException) { + throw SDKError.llm("Failed to register LoRA adapter: ${e.message}") + } }As per coding guidelines: "Use sealed classes for type-safe error handling rather than generic exception handling."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-kotlin/src/jvmAndroidMain/kotlin/com/runanywhere/sdk/public/extensions/RunAnywhere`+LoRA.jvmAndroid.kt around lines 100 - 113, The public method RunAnywhere.registerLoraAdapter currently lets RuntimeException from CppBridgeLoraRegistry.register escape; catch RuntimeException around the CppBridgeLoraRegistry.register call inside RunAnywhere.registerLoraAdapter and convert it into the sealed SDKError.llm (preserving useful error message/details from the caught exception), then rethrow that SDKError.llm so the public API surface is consistent with other LoRA functions like loadLoraAdapter/removeLoraAdapter/clearLoraAdapters; reference CppBridgeLoraRegistry.register and the RunAnywhere.registerLoraAdapter function when making this change.sdk/runanywhere-commons/include/rac/infrastructure/model_management/rac_lora_registry.h (1)
28-38: Add explicit<stddef.h>and<stdint.h>includes.
rac_lora_entry_tusessize_t(from<stddef.h>) andint64_t(from<stdint.h>). As a public C header consumed by external toolchains without the project's include-path configuration, relying on transitive inclusion fromrac_types.his fragile. Explicitly including these headers makes the header self-contained.♻️ Proposed fix
`#include` "rac/core/rac_error.h" `#include` "rac/core/rac_types.h" + +#include <stddef.h> +#include <stdint.h>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-commons/include/rac/infrastructure/model_management/rac_lora_registry.h` around lines 28 - 38, The header declares rac_lora_entry_t and uses size_t and int64_t but doesn't include the standard headers that define them; update the public header to be self-contained by adding explicit includes for <stddef.h> and <stdint.h> at the top of rac_lora_registry.h so size_t and int64_t are guaranteed to be defined for any consumer of rac_lora_entry_t.examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/lora/LoraAdapterPickerSheet.kt (1)
62-62: PrefercollectAsStateWithLifecycle()overcollectAsState()for lifecycle safety.
ChatScreenalready usescollectAsStateWithLifecyclefor the same flow type. Using plaincollectAsState()here keeps the subscription active even when the app is backgrounded.♻️ Proposed change
+import androidx.lifecycle.compose.collectAsStateWithLifecycle ... - val state by loraViewModel.uiState.collectAsState() + val state by loraViewModel.uiState.collectAsStateWithLifecycle()🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/lora/LoraAdapterPickerSheet.kt` at line 62, Replace the lifecycle-unsafe collectAsState() usage on the LoraAdapterPickerSheet UI state with collectAsStateWithLifecycle() to avoid active subscriptions while the app is backgrounded: in the LoraAdapterPickerSheet where you bind val state by loraViewModel.uiState.collectAsState(), change it to use collectAsStateWithLifecycle() from androidx.lifecycle.compose (ensure the import for collectAsStateWithLifecycle is added) so the loraViewModel.uiState flow is observed with lifecycle awareness.examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/navigation/MoreHubScreen.kt (1)
123-129: Consider usingAppColors.primaryPurpleinstead of the hardcoded color.
Color(0xFF8B5CF6)is used here, whileAppColors.primaryPurpleis consistently used for LoRA-related UI elements throughoutLoraAdapterPickerSheetandLoraManagerScreen. Centralising this avoids drift if the brand purple ever changes.🎨 Proposed change
MoreFeatureCard( icon = Icons.Filled.Tune, - iconColor = Color(0xFF8B5CF6), // Purple + iconColor = AppColors.primaryPurple, title = "LoRA Adapters",🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/navigation/MoreHubScreen.kt` around lines 123 - 129, Replace the hardcoded Color(0xFF8B5CF6) used in the MoreFeatureCard call with the shared theme color AppColors.primaryPurple to match LoraAdapterPickerSheet and LoraManagerScreen; update the MoreFeatureCard invocation (iconColor parameter) to use AppColors.primaryPurple so the LoRA-related UI uses the centralized color constant.examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/chat/ChatScreen.kt (1)
125-173: Inconsistent indentation inside theColumnbody.Lines 104–124 (
ChatTopBar) are at 12-space indent inside theColumn, but lines 125–173 (message list and input) are at 16-space indent — one extra level. This doesn't affect runtime behavior but makes the structural nesting ambiguous. Aligning all threeif (uiState.isModelLoaded)blocks to the same indent level would match the surrounding code style.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/chat/ChatScreen.kt` around lines 125 - 173, Align the indentation of the two later "if (uiState.isModelLoaded)" blocks inside the Column so they match the indentation level used for the earlier ChatTopBar block; move the message list/input block (which contains EmptyStateView, LazyColumn, TypingIndicatorView, and Spacer) and the subsequent HorizontalDivider/ChatInputView block to the same indent as the ChatTopBar block to remove the extra nesting level and keep consistent structure around UI elements like MessageBubbleView and calls to viewModel::updateInput and viewModel::sendMessage.examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/lora/LoraManagerScreen.kt (1)
52-53:viewModel()here creates a separateLoraViewModelinstance scoped to this NavBackStackEntry.
ChatScreenholds its ownLoraViewModelinstance. A download started viaChatScreen's LoRA picker won't show in-progress state here. Both instances refresh from the SDK on init so final state converges, but the real-time download progress indicator inLoraManagerScreenwill never reflect an active download triggered fromChatScreen. Consider hoisting the ViewModel to the activity scope (viewModel(LocalActivity.current)) or sharing it via a parent NavBackStackEntry if cross-screen download visibility matters.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/lora/LoraManagerScreen.kt` around lines 52 - 53, LoraManagerScreen currently calls viewModel() which creates a NavBackStackEntry-scoped LoraViewModel separate from the ChatScreen instance; to share real-time download state, hoist the ViewModel to a common scope by obtaining the activity-scoped ViewModel (replace the local viewModel() usage in LoraManagerScreen with the activity-scoped retrieval, e.g., using viewModel(LocalActivity.current) or an equivalent activity-scoped provider) or alternatively resolve the ViewModel from a parent NavBackStackEntry so both ChatScreen and LoraManagerScreen reference the same LoraViewModel instance.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/data/ModelList.kt`:
- Around line 128-137: Remove or gate the LoraAdapterCatalogEntry with id
"uncensored-chat-lora" in ModelList.kt: either delete the entire
LoraAdapterCatalogEntry block (id = "uncensored-chat-lora") or wrap it behind a
build-time or runtime feature flag (e.g., BuildConfig.DEBUG or a dedicated
ENABLE_UNCENSORED_LORA flag) so it is not included in production builds; ensure
any references to this id elsewhere (lookup code that uses
LoraAdapterCatalogEntry) handle the absence or the guarded inclusion
accordingly.
- Around line 162-212: setupModels() lacks error handling so any exception from
RunAnywhere.registerModel/registerMultiFileModel/registerLoraAdapter will leave
_initializationState stuck at Loading; wrap the whole registration loop (or wrap
call site registerModulesAndModels() inside initializeSDK()) with a try-catch
that catches failures, maps them to a sealed InitializationState.Error (or
similar) and assigns _initializationState = Error(...) instead of letting the
exception propagate, and ensure successful completion sets _initializationState
= Ready; reference setupModels(), registerModel, registerMultiFileModel,
registerLoraAdapter, registerModulesAndModels(), initializeSDK(), and the
_initializationState/Loading state when making the change.
- Around line 88-137: The LoraAdapterCatalogEntry entries have incorrect
hardcoded fileSize values causing wrong download/storage estimates; update the
fileSize fields for the entries with ids/filenames: "chat-assistant-lora" /
chat_assistant-lora-Q8_0.gguf -> 673664, "summarizer-lora" /
summarizer-lora-Q8_0.gguf -> 673632, "translator-lora" /
translator-lora-Q8_0.gguf -> 673632, "sentiment-lora" / sentiment-lora-Q8_0.gguf
-> 673632, and "uncensored-chat-lora" / uncensored_chat-lora-Q8_0.gguf ->
1340288 so the LoraAdapterCatalogEntry instances reflect the actual HuggingFace
sizes.
- Around line 87-137: The listed LoraAdapterCatalogEntry entries (ids:
chat-assistant-lora, summarizer-lora, translator-lora, sentiment-lora,
uncensored-chat-lora) are Qwen-trained adapters but their compatibleModelIds
point to LFM2 models; update the catalog so architecture matches: either (A)
replace these adapter entries with LoRA adapters that were trained for LFM2-350M
(swap downloadUrl/filename/fileSize/name/description accordingly) or (B) correct
compatibleModelIds to reference Qwen-family model ids that exist in llmModels so
the adapter and base model architectures match; ensure changes touch the
LoraAdapterCatalogEntry objects and their compatibleModelIds fields accordingly.
- Around line 142-145: The AppModel entry for smolvlm-500m-instruct-q8_0 in
ModelList.kt points to a sherpa-onnx release which is misleading for a GGUF Q8_0
model declared with InferenceFramework.LLAMA_CPP; either host the GGUF artifact
in a conventional location (e.g., HuggingFace or a dedicated llama.cpp release)
and update the url value for AppModel(id = "smolvlm-500m-instruct-q8_0"), or if
the file is truly tied to sherpa-onnx, change the framework value from
InferenceFramework.LLAMA_CPP to the appropriate ONNX framework to avoid
confusion. Ensure the final change updates the url or framework in the AppModel
constructor so id/name remain the same and the source clearly matches the
runtime.
In
`@examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/lora/LoraAdapterPickerSheet.kt`:
- Line 224: The remember call storing scale (var scale by remember {
mutableFloatStateOf(entry.defaultScale) }) lacks keys so it won't reset when
entry.defaultScale changes; update the remember invocation in
LoraAdapterPickerSheet.kt to include a dependency key such as entry.defaultScale
(or entry.id plus entry.defaultScale) so the mutableFloatStateOf is recreated
when the default changes (e.g., use remember(entry.defaultScale) or
remember(entry.id, entry.defaultScale) { mutableFloatStateOf(entry.defaultScale)
} to ensure scale resets correctly).
In
`@examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/lora/LoraViewModel.kt`:
- Around line 154-162: The synchronous File.exists() checks done in
isDownloaded() and via localPath() should be moved out of Compose and computed
once inside refresh()/refreshForModel() to avoid per-recomposition disk I/O: add
a downloadedAdapterIds: Set<String> (or similar) to LoraUiState, compute it in
refresh()/refreshForModel() by iterating registeredAdapters and checking
File(loraDir, it.filename).exists(), then set _uiState.value =
_uiState.value.copy(downloadedAdapterIds = downloaded, ...); update callers
(LoraAdapterPickerSheet, LoraManagerScreen) to use
state.downloadedAdapterIds.contains(entry.id) instead of
isDownloaded(entry)/localPath() and remove or limit File.exists usage from
isDownloaded/localPath so they no longer perform main-thread file I/O during
composition.
- Line 201: The call to tmpFile.renameTo(destFile) in LoraViewModel (and the
later file.delete()) ignores return values so failures are silent; update the
code to check the boolean result of tmpFile.renameTo(destFile) and, on false,
attempt a safe fallback (e.g., copy stream from tmpFile to destFile then delete
tmpFile) and log an error/warning via your logger with context (include tmpFile
and destFile paths), and likewise check file.delete() return value and log a
warning if it returns false so stale .tmp files are visible and handled.
- Around line 93-135: The JNI/blocking calls (RunAnywhere.loadLoraAdapter,
RunAnywhere.removeLoraAdapter, RunAnywhere.clearLoraAdapters and any file ops in
deleteAdapter) are currently invoked on viewModelScope's Main dispatcher; wrap
those calls in withContext(Dispatchers.IO) so the heavy JNI/file operations run
off the main thread, then switch back to update _uiState/Log on the main thread;
do the same for getLoadedLoraAdapters() where it may hit I/O in
refresh/refreshForModel. Locate these in loadAdapter, unloadAdapter, clearAll,
deleteAdapter (and optionally refresh/refreshForModel) and move only the
blocking calls into withContext(Dispatchers.IO) while keeping UI state
assignments on the original coroutine context.
In
`@sdk/runanywhere-commons/src/infrastructure/model_management/lora_registry.cpp`:
- Around line 33-44: deep_copy_lora_entry can return a partially-initialized
entry when malloc or rac_strdup fails (leaving compatible_model_ids == nullptr
and compatible_model_count == 0), causing silent corruption in callers like
rac_lora_registry_register/get_all/get_for_model; modify deep_copy_lora_entry so
that any allocation failure (the calloc for copy, the malloc for
compatible_model_ids, or any rac_strdup inside the loop) cleans up all
previously allocated resources (free any duplicated strings, free
compatible_model_ids if allocated, free the top-level copy) and returns nullptr;
also ensure compatible_model_count is only set after a successful full copy and
do explicit nullptr checks after every rac_strdup call so partial progress is
not left behind.
In `@sdk/runanywhere-commons/src/jni/runanywhere_commons_jni.cpp`:
- Around line 1972-1976: The racModelAssignmentFetch function currently emits
camelCase keys and omits local_path, supportsLora, and description which breaks
CppBridgeModelRegistry's parser; update racModelAssignmentFetch to produce the
exact snake_case JSON and include all fields (download_url, download_size,
context_length, local_path, supports_lora, description) by either calling the
existing modelInfoToJson helper for each model or replicating its exact
serialization logic so the output matches modelInfoToJson and the Kotlin/C++
parsers.
In
`@sdk/runanywhere-kotlin/src/commonMain/kotlin/com/runanywhere/sdk/public/extensions/Models/ModelTypes.kt`:
- Around line 347-349: The Kotlin ModelInfo definition diverges from the Swift
source by including supportsLora; remove the supportsLora property from the
ModelInfo data class in ModelTypes.kt so the class only has supportsThinking and
thinkingPattern (and update the primary constructor accordingly), search for and
delete or refactor any usages of supportsLora elsewhere to use the remaining
flags, and update the file header comment that claims it “Mirrors Swift
ModelTypes.swift exactly” if still present to reflect the corrected mirror.
In
`@sdk/runanywhere-kotlin/src/jvmAndroidMain/kotlin/com/runanywhere/sdk/foundation/bridge/extensions/CppBridgeLoraRegistry.kt`:
- Around line 73-118: The JSON parsing is broken because extractString uses a
naive pattern that fails on escaped quotes and
parseLoraEntryArrayJson/extractStringArray treat braces/brackets inside quoted
strings as structural; update extractString to use the same robust string regex
as CppBridgeLLM (pattern like (?:[^"\\]|\\.)* so escaped quotes are handled),
and make parseLoraEntryArrayJson and extractStringArray iterate
character-by-character while tracking whether the parser is currently inside a
quoted string (and skip structural depth changes when inside quotes) and also
correctly handle escaped quote characters; modify the functions named
parseLoraEntryArrayJson, extractStringArray and extractString accordingly so
they ignore structural characters inside quoted strings and accept escaped
characters in string values.
---
Duplicate comments:
In
`@examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/chat/ChatViewModel.kt`:
- Around line 803-813: handleLLMEvent currently never clears LoRA state on model
unload, so currentModelSupportsLora and hasActiveLoraAdapter stay stale; add a
branch in ChatViewModel.handleLLMEvent that detects the model-unloaded/unload
lifecycle event from LLMEvent.LLMEventType (use the enum/variant name if
present) and reset currentModelSupportsLora = false and _uiState.value =
_uiState.value.copy(hasActiveLoraAdapter = false) (or call refreshLoraState()
after clearing) so indicators are cleared immediately when a model is unloaded;
reference the handleLLMEvent method and refreshLoraState function to locate
where to add this case.
In
`@examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/lora/LoraManagerScreen.kt`:
- Line 55: The UI state in LoraManagerScreen is using collectAsState() which can
observe flows outside the Compose lifecycle; replace
loraViewModel.uiState.collectAsState() with
loraViewModel.uiState.collectAsStateWithLifecycle() (from
androidx.lifecycle.compose) to ensure lifecycle-aware collection. Update the
import and any qualifiers in LoraManagerScreen to use
collectAsStateWithLifecycle(), mirroring the fix already applied in
LoraAdapterPickerSheet.
In
`@examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/lora/LoraViewModel.kt`:
- Around line 191-198: The download loop in LoraViewModel.kt (inside the
withContext(Dispatchers.IO) block that performs the streaming read) fails to
check coroutine cancellation, so cancelling downloadJob via cancelDownload()
won't interrupt the blocking input.read call; fix this by calling ensureActive()
(from kotlinx.coroutines) inside the while loop that reads from input (the loop
using input.read(buffer).also { bytesRead = it }), before doing output.write and
progress updates, so a CancellationException is thrown and the coroutine exits
promptly when downloadJob is cancelled.
In `@sdk/runanywhere-commons/include/rac/core/rac_core.h`:
- Around line 338-346: Add the missing type include and detailed docs: include
the header that defines struct rac_lora_entry (e.g.
"rac/infrastructure/model_management/rac_lora_registry.h") so callers can
construct and dereference rac_lora_entry, then add Doxygen-style documentation
for rac_get_lora_registry, rac_register_lora, and rac_get_lora_for_model that
includes `@brief`, `@param`, `@return`, error codes, and explicit ownership/lifetime
notes—specifically document whether the returned array from
rac_get_lora_for_model (out_entries/out_count) is owned by the caller and how to
free it (mirror rac_get_model’s note such as “owned, must be freed with
rac_model_info_free” or provide the correct rac_lora free function), and
document any vtable/lifecycle expectations for rac_lora_registry.
In `@sdk/runanywhere-commons/include/rac/features/llm/rac_llm_component.h`:
- Around line 266-279: The docblock for rac_llm_component_check_lora_compat is
inaccurate and incomplete: update the comment for
rac_llm_component_check_lora_compat to reflect the actual behavior (it does not
check file existence) and enumerate all returned error codes
(RAC_ERROR_INVALID_HANDLE, RAC_ERROR_INVALID_ARGUMENT,
RAC_ERROR_COMPONENT_NOT_READY, RAC_ERROR_NOT_SUPPORTED, and RAC_SUCCESS), plus
lifecycle/vtable expectations for the LLM component API; state that the function
performs a lightweight compatibility/preload check (not a filesystem existence
check), describe ownership/alloc/free semantics for out_error (caller must free
with rac_free), and mention any preconditions (component initialized/ready) and
the related vtable function if applicable (rac_llm_component_check_lora_compat).
In
`@sdk/runanywhere-commons/include/rac/infrastructure/model_management/rac_lora_registry.h`:
- Around line 42-67: The public header lacks per-function documentation for
error codes, preconditions, and ownership semantics; update
rac_lora_registry_create/destroy, rac_lora_registry_register/remove,
rac_lora_registry_get_all/get_for_model/get, and
rac_lora_entry_free/array_free/rac_lora_entry_copy with clear doc comments that
list possible rac_result_t return values, null-pointer preconditions, and
precise ownership rules (e.g., whether register deep-copies the
rac_lora_entry_t, who frees returned rac_lora_entry_t* from
rac_lora_registry_get, and that arrays from
rac_lora_registry_get_all/get_for_model must be freed with
rac_lora_entry_array_free), and include vtable/ABI stability notes per our
include/rac/ header guidelines.
In `@sdk/runanywhere-commons/src/features/llm/llm_component.cpp`:
- Around line 879-889: Move the adapter_path argument validation before
acquiring lifecycle/service state so an empty adapter_path returns the correct
error: check strlen(adapter_path) == 0 and set *out_error = strdup("Empty
adapter path"); return RAC_ERROR_INVALID_ARGUMENT before calling
rac_lifecycle_get_service or any locks; keep the existing messages and error
codes (RAC_ERROR_INVALID_ARGUMENT vs RAC_ERROR_COMPONENT_NOT_READY) and update
the function containing rac_lifecycle_get_service, adapter_path, and out_error
accordingly.
In
`@sdk/runanywhere-commons/src/infrastructure/model_management/lora_registry.cpp`:
- Around line 19-21: The registry currently uses a raw string key (entries:
std::map<std::string, rac_lora_entry_t*>) which violates the structured-types
guideline; replace the string key with a dedicated ID type (e.g., rac_lora_id_t
or a struct/class representing the LORA ID) in the rac_lora_registry::entries
map and update all lookup/compare code to use that type; specifically change the
map declaration in struct rac_lora_registry and then adjust any functions that
access entries (constructors, find/insert/erase calls, and comparisons) to
accept/return the new rac_lora_id_t instead of std::string, updating conversions
at the boundary only.
In `@sdk/runanywhere-commons/src/jni/runanywhere_commons_jni.cpp`:
- Around line 1953-1970: loraEntryToJson currently returns a JSON string; change
it to return a structured JSON object (e.g., nlohmann::json) instead of
std::string so callers can work with typed data: update loraEntryToJson
signature to return nlohmann::json, build and return a JSON object (not
j.dump()) with fields id, name, description, download_url, filename, file_size,
default_scale and compatible_model_ids as an array, and then update
racLoraRegistryGetAll and racLoraRegistryGetForModel to consume the
nlohmann::json results (or convert them to the proper JNI jobject via your
existing to-JNI helper) instead of treating them as string blobs.
- Around line 1288-1328: The JNI bridge is missing wrappers for
rac_lora_registry_remove and rac_lora_registry_get; add two JNI functions
mirroring the style of
Java_com_runanywhere_sdk_native_bridge_RunAnywhereBridge_racLoraRegistryGetForModel
and _racLoraRegistryGetAll that call rac_lora_registry_remove(registry, id) and
rac_lora_registry_get(registry, id, &entry) respectively, properly marshal Java
strings with GetStringUTFChars/ReleaseStringUTFChars, validate registry and C
API return codes, convert returned rac_lora_entry_t* to JSON via loraEntryToJson
(and free with rac_lora_entry_free or rac_lora_entry_array_free as appropriate),
and return env->NewStringUTF("[]") on errors to match existing behavior.
In
`@sdk/runanywhere-kotlin/src/jvmAndroidMain/kotlin/com/runanywhere/sdk/public/extensions/RunAnywhere`+LoRA.jvmAndroid.kt:
- Around line 100-121: The three JNI-backed functions
(RunAnywhere.registerLoraAdapter, RunAnywhere.loraAdaptersForModel,
RunAnywhere.allRegisteredLoraAdapters) currently call CppBridgeLoraRegistry
directly and can surface UnsatisfiedLinkError when the SDK isn't initialized;
add the same isInitialized guard used elsewhere so they first check if
(isInitialized) or else throw SDKError.notInitialized (or return the appropriate
error) before invoking CppBridgeLoraRegistry.register,
CppBridgeLoraRegistry.getForModel, or CppBridgeLoraRegistry.getAll,
respectively, ensuring the SDK error is raised instead of a JNI error.
---
Nitpick comments:
In `@build.gradle.kts`:
- Around line 160-162: The task buildFullSdk is currently placed in group
"native" but also builds the Kotlin SDK; change its group assignment to a more
appropriate scope (e.g., "sdk" or a new "pipeline" group) so it appears when
users list sdk tasks; locate the tasks.register("buildFullSdk") block and
replace group = "native" with group = "sdk" (or group = "pipeline") and update
the description if you create a new group to keep semantics clear.
- Around line 146-186: The three tasks buildCpp, buildFullSdk, and
copyNativeLibs currently call scripts directly and will throw a raw exec
exception if the target scripts are missing; add the same existence guard used
by the setup task: before calling exec, locate the script file in the workingDir
(e.g., "sdk/runanywhere-kotlin/scripts/build-sdk.sh" or
"scripts/build-kotlin.sh"), check file.exists(), and if not print a clear
warning via logger/println and return from doLast so the task no-ops instead of
failing; keep existing environment setup (resolveNdkHome/resolveAndroidHome) and
exec invocation otherwise.
- Around line 174-185: The copyNativeLibs task currently injects
ANDROID_NDK_HOME even though it runs build-kotlin.sh with --skip-build; remove
the environment("ANDROID_NDK_HOME", ndkHome) line from the exec block in the
tasks.register("copyNativeLibs") configuration so the copy-only task does not
depend on the NDK; if other tasks or scripts still need ANDROID_NDK_HOME, set it
only in the tasks that perform native builds or in the build script paths that
call resolveNdkHome/resolveAndroidHome instead.
In
`@examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/chat/ChatScreen.kt`:
- Around line 125-173: Align the indentation of the two later "if
(uiState.isModelLoaded)" blocks inside the Column so they match the indentation
level used for the earlier ChatTopBar block; move the message list/input block
(which contains EmptyStateView, LazyColumn, TypingIndicatorView, and Spacer) and
the subsequent HorizontalDivider/ChatInputView block to the same indent as the
ChatTopBar block to remove the extra nesting level and keep consistent structure
around UI elements like MessageBubbleView and calls to viewModel::updateInput
and viewModel::sendMessage.
In
`@examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/lora/LoraAdapterPickerSheet.kt`:
- Line 62: Replace the lifecycle-unsafe collectAsState() usage on the
LoraAdapterPickerSheet UI state with collectAsStateWithLifecycle() to avoid
active subscriptions while the app is backgrounded: in the
LoraAdapterPickerSheet where you bind val state by
loraViewModel.uiState.collectAsState(), change it to use
collectAsStateWithLifecycle() from androidx.lifecycle.compose (ensure the import
for collectAsStateWithLifecycle is added) so the loraViewModel.uiState flow is
observed with lifecycle awareness.
In
`@examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/lora/LoraManagerScreen.kt`:
- Around line 52-53: LoraManagerScreen currently calls viewModel() which creates
a NavBackStackEntry-scoped LoraViewModel separate from the ChatScreen instance;
to share real-time download state, hoist the ViewModel to a common scope by
obtaining the activity-scoped ViewModel (replace the local viewModel() usage in
LoraManagerScreen with the activity-scoped retrieval, e.g., using
viewModel(LocalActivity.current) or an equivalent activity-scoped provider) or
alternatively resolve the ViewModel from a parent NavBackStackEntry so both
ChatScreen and LoraManagerScreen reference the same LoraViewModel instance.
In
`@examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/navigation/MoreHubScreen.kt`:
- Around line 123-129: Replace the hardcoded Color(0xFF8B5CF6) used in the
MoreFeatureCard call with the shared theme color AppColors.primaryPurple to
match LoraAdapterPickerSheet and LoraManagerScreen; update the MoreFeatureCard
invocation (iconColor parameter) to use AppColors.primaryPurple so the
LoRA-related UI uses the centralized color constant.
In
`@examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/theme/Dimensions.kt`:
- Around line 109-110: assistantIconSize (20.dp) is declared inline in the chat
section and assistantIconSpacing duplicates the existing medium constant; move
or align these with the ICON SIZES group: either add a named 20.dp constant to
the ICON SIZES block (if reuse is expected) and replace inline assistantIconSize
with that constant, or leave assistantIconSize inline if chat-only, and replace
assistantIconSpacing with the existing medium constant (e.g., val
assistantIconSpacing = medium) to avoid a silent duplicate; update usages of
assistantIconSize and assistantIconSpacing accordingly.
- Line 113: userBubbleCornerRadius is a redundant alias of
messageBubbleCornerRadius (both set to cornerRadiusBubble); either remove
userBubbleCornerRadius and reference messageBubbleCornerRadius directly at call
sites, or keep userBubbleCornerRadius but add a clear comment above it
explaining it intentionally aliases messageBubbleCornerRadius for future
independent tuning—refer to the symbols userBubbleCornerRadius,
messageBubbleCornerRadius, and cornerRadiusBubble to locate and update the
declaration.
In
`@sdk/runanywhere-commons/include/rac/infrastructure/model_management/rac_lora_registry.h`:
- Around line 28-38: The header declares rac_lora_entry_t and uses size_t and
int64_t but doesn't include the standard headers that define them; update the
public header to be self-contained by adding explicit includes for <stddef.h>
and <stdint.h> at the top of rac_lora_registry.h so size_t and int64_t are
guaranteed to be defined for any consumer of rac_lora_entry_t.
In
`@sdk/runanywhere-kotlin/src/jvmAndroidMain/kotlin/com/runanywhere/sdk/foundation/bridge/extensions/CppBridgeLoraRegistry.kt`:
- Around line 92-129: Multiple JSON parsing helpers (extractString, extractLong,
extractFloat, extractStringArray) and logging utilities (LogLevel enum and log
function) are duplicated from CppBridgeModelRegistry; extract them into a single
shared utility (e.g., CppBridgeJsonUtils) in jvmAndroidMain exposing those
functions and a log wrapper, then replace the local implementations in
CppBridgeLoraRegistry.kt and CppBridgeModelRegistry.kt to call the shared
methods (use the same function names: extractString, extractLong, extractFloat,
extractStringArray and the LogLevel/log entrypoint so callers need only import
and delegate to the new shared object).
In
`@sdk/runanywhere-kotlin/src/jvmAndroidMain/kotlin/com/runanywhere/sdk/public/extensions/RunAnywhere`+LoRA.jvmAndroid.kt:
- Around line 100-113: The public method RunAnywhere.registerLoraAdapter
currently lets RuntimeException from CppBridgeLoraRegistry.register escape;
catch RuntimeException around the CppBridgeLoraRegistry.register call inside
RunAnywhere.registerLoraAdapter and convert it into the sealed SDKError.llm
(preserving useful error message/details from the caught exception), then
rethrow that SDKError.llm so the public API surface is consistent with other
LoRA functions like loadLoraAdapter/removeLoraAdapter/clearLoraAdapters;
reference CppBridgeLoraRegistry.register and the RunAnywhere.registerLoraAdapter
function when making this change.
...ples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/data/ModelList.kt
Outdated
Show resolved
Hide resolved
| LoraAdapterCatalogEntry( | ||
| id = "chat-assistant-lora", | ||
| name = "Chat Assistant", | ||
| description = "Enhances conversational chat ability", | ||
| downloadUrl = "https://huggingface.co/Void2377/Qwen/resolve/main/lora/chat_assistant-lora-Q8_0.gguf", | ||
| filename = "chat_assistant-lora-Q8_0.gguf", | ||
| compatibleModelIds = listOf("lfm2-350m-q4_k_m", "lfm2-350m-q8_0"), | ||
| fileSize = 690_176, | ||
| defaultScale = 1.0f, | ||
| ), | ||
| LoraAdapterCatalogEntry( | ||
| id = "summarizer-lora", | ||
| name = "Summarizer", | ||
| description = "Specialized for text summarization tasks", | ||
| downloadUrl = "https://huggingface.co/Void2377/Qwen/resolve/main/lora/summarizer-lora-Q8_0.gguf", | ||
| filename = "summarizer-lora-Q8_0.gguf", | ||
| compatibleModelIds = listOf("lfm2-350m-q4_k_m", "lfm2-350m-q8_0"), | ||
| fileSize = 690_176, | ||
| defaultScale = 1.0f, | ||
| ), | ||
| LoraAdapterCatalogEntry( | ||
| id = "translator-lora", | ||
| name = "Translator", | ||
| description = "Improves translation between languages", | ||
| downloadUrl = "https://huggingface.co/Void2377/Qwen/resolve/main/lora/translator-lora-Q8_0.gguf", | ||
| filename = "translator-lora-Q8_0.gguf", | ||
| compatibleModelIds = listOf("lfm2-350m-q4_k_m", "lfm2-350m-q8_0"), | ||
| fileSize = 690_176, | ||
| defaultScale = 1.0f, | ||
| ), | ||
| LoraAdapterCatalogEntry( | ||
| id = "sentiment-lora", | ||
| name = "Sentiment Analysis", | ||
| description = "Fine-tuned for sentiment analysis tasks", | ||
| downloadUrl = "https://huggingface.co/Void2377/Qwen/resolve/main/lora/sentiment-lora-Q8_0.gguf", | ||
| filename = "sentiment-lora-Q8_0.gguf", | ||
| compatibleModelIds = listOf("lfm2-350m-q4_k_m", "lfm2-350m-q8_0"), | ||
| fileSize = 690_176, | ||
| defaultScale = 1.0f, | ||
| ), | ||
| LoraAdapterCatalogEntry( | ||
| id = "uncensored-chat-lora", | ||
| name = "Uncensored Chat", | ||
| description = "Removes safety guardrails for uncensored responses", | ||
| downloadUrl = "https://huggingface.co/Void2377/Qwen/resolve/main/lora/uncensored_chat-lora-Q8_0.gguf", | ||
| filename = "uncensored_chat-lora-Q8_0.gguf", | ||
| compatibleModelIds = listOf("lfm2-350m-q4_k_m", "lfm2-350m-q8_0"), | ||
| fileSize = 1_372_160, | ||
| defaultScale = 1.0f, | ||
| ), |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Fetch Content-Length headers for each adapter to compare against hardcoded sizes.
for filename in \
"chat_assistant-lora-Q8_0.gguf" \
"summarizer-lora-Q8_0.gguf" \
"translator-lora-Q8_0.gguf" \
"sentiment-lora-Q8_0.gguf" \
"uncensored_chat-lora-Q8_0.gguf"; do
echo "=== $filename ==="
curl -sI "https://huggingface.co/Void2377/Qwen/resolve/main/lora/$filename" \
| grep -iE "(content-length|http)" | head -2
doneRepository: RunanywhereAI/runanywhere-sdks
Length of output: 425
🏁 Script executed:
#!/bin/bash
# Fetch Content-Length headers following redirects
for filename in \
"chat_assistant-lora-Q8_0.gguf" \
"summarizer-lora-Q8_0.gguf" \
"translator-lora-Q8_0.gguf" \
"sentiment-lora-Q8_0.gguf" \
"uncensored_chat-lora-Q8_0.gguf"; do
echo "=== $filename ==="
curl -sIL "https://huggingface.co/Void2377/Qwen/resolve/main/lora/$filename" \
| grep -iE "(content-length|http)" | tail -2
doneRepository: RunanywhereAI/runanywhere-sdks
Length of output: 444
Update all hardcoded fileSize values to match actual HuggingFace file sizes.
The hardcoded file sizes are incorrect across all five adapters:
chat-assistant-lora-Q8_0.gguf: should be673664(not690_176)summarizer-lora-Q8_0.gguf: should be673632(not690_176)translator-lora-Q8_0.gguf: should be673632(not690_176)sentiment-lora-Q8_0.gguf: should be673632(not690_176)uncensored_chat-lora-Q8_0.gguf: should be1340288(not1_372_160)
If the SDK uses fileSize for download progress estimation, storage reservation, or integrity checks, these inaccurate values will produce misleading UX or verification failures.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/data/ModelList.kt`
around lines 88 - 137, The LoraAdapterCatalogEntry entries have incorrect
hardcoded fileSize values causing wrong download/storage estimates; update the
fileSize fields for the entries with ids/filenames: "chat-assistant-lora" /
chat_assistant-lora-Q8_0.gguf -> 673664, "summarizer-lora" /
summarizer-lora-Q8_0.gguf -> 673632, "translator-lora" /
translator-lora-Q8_0.gguf -> 673632, "sentiment-lora" / sentiment-lora-Q8_0.gguf
-> 673632, and "uncensored-chat-lora" / uncensored_chat-lora-Q8_0.gguf ->
1340288 so the LoraAdapterCatalogEntry instances reflect the actual HuggingFace
sizes.
...ples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/data/ModelList.kt
Outdated
Show resolved
Hide resolved
| AppModel(id = "smolvlm-500m-instruct-q8_0", name = "SmolVLM 500M Instruct", | ||
| url = "https://github.com/RunanywhereAI/sherpa-onnx/releases/download/runanywhere-vlm-models-v1/smolvlm-500m-instruct-q8_0.tar.gz", | ||
| framework = InferenceFramework.LLAMA_CPP, category = ModelCategory.MULTIMODAL, | ||
| memoryRequirement = 600_000_000), |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
curl -sI "https://github.com/RunanywhereAI/sherpa-onnx/releases/download/runanywhere-vlm-models-v1/smolvlm-500m-instruct-q8_0.tar.gz" 2>&1 | head -20Repository: RunanywhereAI/runanywhere-sdks
Length of output: 5112
🌐 Web query:
SmolVLM 500M GGUF quantization format model
💡 Result:
SmolVLM 500M is available in GGUF (llama.cpp) format on Hugging Face:
- Official-ish ggml-org build:
ggml-org/SmolVLM-500M-Instruct-GGUF— provides F16 and Q8_0 model weights plus the required mmproj files (vision projector) in f16 and Q8_0. [1] - Community repo with many quantization options:
Mungert/SmolVLM-500M-Instruct-GGUF— includes lots of GGUF quants such as q4_0, q4_k_m, q5_k_m, q6_k_m, q8_0, plus iq3/iq4 variants, and mmproj files. [2]
To run SmolVLM in llama.cpp you typically need both:
- the LLM weights:
SmolVLM-500M-Instruct-*.ggufand - the vision projector:
mmproj-SmolVLM-500M-Instruct-*.gguf. [3]
Sources: [1] [2] [3]
Consider hosting SmolVLM in a more conventional location.
The model is correctly formatted as GGUF (Q8_0 quantization), which is appropriate for the LLAMA_CPP framework. However, hosting it in a sherpa-onnx release is unconventional since sherpa-onnx is an ONNX inference engine, not llama.cpp. This may confuse developers about which framework actually runs the model. Consider moving it to HuggingFace or a dedicated llama.cpp release for clarity.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/data/ModelList.kt`
around lines 142 - 145, The AppModel entry for smolvlm-500m-instruct-q8_0 in
ModelList.kt points to a sherpa-onnx release which is misleading for a GGUF Q8_0
model declared with InferenceFramework.LLAMA_CPP; either host the GGUF artifact
in a conventional location (e.g., HuggingFace or a dedicated llama.cpp release)
and update the url value for AppModel(id = "smolvlm-500m-instruct-q8_0"), or if
the file is truly tied to sherpa-onnx, change the framework value from
InferenceFramework.LLAMA_CPP to the appropriate ONNX framework to avoid
confusion. Ensure the final change updates the url or framework in the AppModel
constructor so id/name remain the same and the source clearly matches the
runtime.
...ples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/data/ModelList.kt
Show resolved
Hide resolved
| } | ||
| } | ||
| } | ||
| tmpFile.renameTo(destFile) |
There was a problem hiding this comment.
tmpFile.renameTo(destFile) return value is unchecked — silent download failure.
On Android, File.renameTo can fail (e.g., source and destination on different partitions) and returns false without throwing. If it fails, destFile never exists and the UI shows success while the adapter is unusable and the .tmp file is left on disk taking up space.
🐛 Proposed fix
- tmpFile.renameTo(destFile)
+ if (!tmpFile.renameTo(destFile)) {
+ tmpFile.copyTo(destFile, overwrite = true)
+ tmpFile.delete()
+ }Also applies to file.delete() at line 241 — log a warning if the delete returns false to avoid a silent stale-file situation.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| tmpFile.renameTo(destFile) | |
| if (!tmpFile.renameTo(destFile)) { | |
| tmpFile.copyTo(destFile, overwrite = true) | |
| tmpFile.delete() | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/lora/LoraViewModel.kt`
at line 201, The call to tmpFile.renameTo(destFile) in LoraViewModel (and the
later file.delete()) ignores return values so failures are silent; update the
code to check the boolean result of tmpFile.renameTo(destFile) and, on false,
attempt a safe fallback (e.g., copy stream from tmpFile to destFile then delete
tmpFile) and log an error/warning via your logger with context (include tmpFile
and destFile paths), and likewise check file.delete() return value and log a
warning if it returns false so stale .tmp files are visible and handled.
sdk/runanywhere-commons/src/infrastructure/model_management/lora_registry.cpp
Show resolved
Hide resolved
| val supportsThinking: Boolean = false, | ||
| val supportsLora: Boolean = false, | ||
| val thinkingPattern: ThinkingTagPattern? = null, |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
rg -n "supportsLora" --type swiftRepository: RunanywhereAI/runanywhere-sdks
Length of output: 56
🏁 Script executed:
#!/bin/bash
# Find Swift ModelTypes file
fd "ModelTypes" --type f --extension swiftRepository: RunanywhereAI/runanywhere-sdks
Length of output: 345
🏁 Script executed:
#!/bin/bash
# Read the Kotlin file header around line 9 to see the assertion
head -20 sdk/runanywhere-kotlin/src/commonMain/kotlin/com/runanywhere/sdk/public/extensions/Models/ModelTypes.ktRepository: RunanywhereAI/runanywhere-sdks
Length of output: 621
🏁 Script executed:
#!/bin/bash
# Read the Swift ModelTypes.swift file
cat -n sdk/runanywhere-swift/Sources/RunAnywhere/Public/Extensions/Models/ModelTypes.swiftRepository: RunanywhereAI/runanywhere-sdks
Length of output: 20673
🏁 Script executed:
#!/bin/bash
# Read Kotlin ModelInfo data class definition
sed -n '330,380p' sdk/runanywhere-kotlin/src/commonMain/kotlin/com/runanywhere/sdk/public/extensions/Models/ModelTypes.ktRepository: RunanywhereAI/runanywhere-sdks
Length of output: 1975
Remove the supportsLora field from Kotlin ModelInfo — it does not exist in the Swift source of truth.
The Swift ModelInfo struct (sdk/runanywhere-swift/Sources/RunAnywhere/Public/Extensions/Models/ModelTypes.swift) contains only supportsThinking and thinkingPattern for capability flags. The supportsLora field in the Kotlin implementation introduces divergence and violates the coding guideline to follow iOS as the source of truth for business logic. The file header's assertion that it "Mirrors Swift ModelTypes.swift exactly" is also now false.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@sdk/runanywhere-kotlin/src/commonMain/kotlin/com/runanywhere/sdk/public/extensions/Models/ModelTypes.kt`
around lines 347 - 349, The Kotlin ModelInfo definition diverges from the Swift
source by including supportsLora; remove the supportsLora property from the
ModelInfo data class in ModelTypes.kt so the class only has supportsThinking and
thinkingPattern (and update the primary constructor accordingly), search for and
delete or refactor any usages of supportsLora elsewhere to use the remaining
flags, and update the file header comment that claims it “Mirrors Swift
ModelTypes.swift exactly” if still present to reflect the corrected mirror.
| private fun parseLoraEntryArrayJson(json: String): List<LoraEntry> { | ||
| if (json == "[]" || json.isBlank()) return emptyList() | ||
| val entries = mutableListOf<LoraEntry>() | ||
| var depth = 0; var objectStart = -1 | ||
| for (i in json.indices) { | ||
| when (json[i]) { | ||
| '{' -> { if (depth == 0) objectStart = i; depth++ } | ||
| '}' -> { | ||
| depth-- | ||
| if (depth == 0 && objectStart >= 0) { | ||
| parseLoraEntryJson(json.substring(objectStart, i + 1))?.let { entries.add(it) } | ||
| objectStart = -1 | ||
| } | ||
| } | ||
| } | ||
| } | ||
| return entries | ||
| } | ||
|
|
||
| private fun extractString(json: String, key: String): String? { | ||
| val regex = Regex(""""$key"\s*:\s*"([^"]*)"""") | ||
| return regex.find(json)?.groupValues?.get(1)?.takeIf { it.isNotEmpty() } | ||
| } | ||
|
|
||
| private fun extractLong(json: String, key: String): Long { | ||
| val regex = Regex(""""$key"\s*:\s*(-?\d+)""") | ||
| return regex.find(json)?.groupValues?.get(1)?.toLongOrNull() ?: 0L | ||
| } | ||
|
|
||
| private fun extractFloat(json: String, key: String): Float { | ||
| val regex = Regex(""""$key"\s*:\s*(-?[\d.]+)""") | ||
| return regex.find(json)?.groupValues?.get(1)?.toFloatOrNull() ?: 0f | ||
| } | ||
|
|
||
| private fun extractStringArray(json: String, key: String): List<String> { | ||
| val keyMatch = Regex(""""$key"\s*:\s*\[""").find(json) ?: return emptyList() | ||
| val arrayStart = keyMatch.range.last + 1 | ||
| var depth = 1; var pos = arrayStart | ||
| while (pos < json.length && depth > 0) { | ||
| when (json[pos]) { '[' -> depth++; ']' -> depth-- }; pos++ | ||
| } | ||
| if (depth != 0) return emptyList() | ||
| val arrayContent = json.substring(arrayStart, pos - 1).trim() | ||
| if (arrayContent.isEmpty()) return emptyList() | ||
| return Regex(""""([^"]*)"""").findAll(arrayContent).map { it.groupValues[1] }.toList() | ||
| } |
There was a problem hiding this comment.
JSON parsing is fragile: special characters inside string values corrupt extraction.
Two related issues:
-
extractString(Line 93) — the[^"]*pattern cannot match JSON string values containing escaped quotes (e.g.,"description": "Say \"hello\"").CppBridgeLLM.ktalready uses the correct pattern(?:[^"\\]|\\.)*for this purpose. -
parseLoraEntryArrayJson/extractStringArray(Lines 79, 112) — the depth counters treat every bare{,},[,]as structural, including those inside quoted string values. A description such as"Works with {braces}"would desync the depth counter and cause the enclosing object to be missed.
🛠 Proposed fix for extractString
private fun extractString(json: String, key: String): String? {
- val regex = Regex(""""$key"\s*:\s*"([^"]*)"""")
+ val regex = Regex(""""$key"\s*:\s*"((?:[^"\\]|\\.)*)"""")
return regex.find(json)?.groupValues?.get(1)?.takeIf { it.isNotEmpty() }
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| private fun parseLoraEntryArrayJson(json: String): List<LoraEntry> { | |
| if (json == "[]" || json.isBlank()) return emptyList() | |
| val entries = mutableListOf<LoraEntry>() | |
| var depth = 0; var objectStart = -1 | |
| for (i in json.indices) { | |
| when (json[i]) { | |
| '{' -> { if (depth == 0) objectStart = i; depth++ } | |
| '}' -> { | |
| depth-- | |
| if (depth == 0 && objectStart >= 0) { | |
| parseLoraEntryJson(json.substring(objectStart, i + 1))?.let { entries.add(it) } | |
| objectStart = -1 | |
| } | |
| } | |
| } | |
| } | |
| return entries | |
| } | |
| private fun extractString(json: String, key: String): String? { | |
| val regex = Regex(""""$key"\s*:\s*"([^"]*)"""") | |
| return regex.find(json)?.groupValues?.get(1)?.takeIf { it.isNotEmpty() } | |
| } | |
| private fun extractLong(json: String, key: String): Long { | |
| val regex = Regex(""""$key"\s*:\s*(-?\d+)""") | |
| return regex.find(json)?.groupValues?.get(1)?.toLongOrNull() ?: 0L | |
| } | |
| private fun extractFloat(json: String, key: String): Float { | |
| val regex = Regex(""""$key"\s*:\s*(-?[\d.]+)""") | |
| return regex.find(json)?.groupValues?.get(1)?.toFloatOrNull() ?: 0f | |
| } | |
| private fun extractStringArray(json: String, key: String): List<String> { | |
| val keyMatch = Regex(""""$key"\s*:\s*\[""").find(json) ?: return emptyList() | |
| val arrayStart = keyMatch.range.last + 1 | |
| var depth = 1; var pos = arrayStart | |
| while (pos < json.length && depth > 0) { | |
| when (json[pos]) { '[' -> depth++; ']' -> depth-- }; pos++ | |
| } | |
| if (depth != 0) return emptyList() | |
| val arrayContent = json.substring(arrayStart, pos - 1).trim() | |
| if (arrayContent.isEmpty()) return emptyList() | |
| return Regex(""""([^"]*)"""").findAll(arrayContent).map { it.groupValues[1] }.toList() | |
| } | |
| private fun parseLoraEntryArrayJson(json: String): List<LoraEntry> { | |
| if (json == "[]" || json.isBlank()) return emptyList() | |
| val entries = mutableListOf<LoraEntry>() | |
| var depth = 0; var objectStart = -1 | |
| for (i in json.indices) { | |
| when (json[i]) { | |
| '{' -> { if (depth == 0) objectStart = i; depth++ } | |
| '}' -> { | |
| depth-- | |
| if (depth == 0 && objectStart >= 0) { | |
| parseLoraEntryJson(json.substring(objectStart, i + 1))?.let { entries.add(it) } | |
| objectStart = -1 | |
| } | |
| } | |
| } | |
| } | |
| return entries | |
| } | |
| private fun extractString(json: String, key: String): String? { | |
| val regex = Regex(""""$key"\s*:\s*"((?:[^"\\]|\\.)*)"""") | |
| return regex.find(json)?.groupValues?.get(1)?.takeIf { it.isNotEmpty() } | |
| } | |
| private fun extractLong(json: String, key: String): Long { | |
| val regex = Regex(""""$key"\s*:\s*(-?\d+)""") | |
| return regex.find(json)?.groupValues?.get(1)?.toLongOrNull() ?: 0L | |
| } | |
| private fun extractFloat(json: String, key: String): Float { | |
| val regex = Regex(""""$key"\s*:\s*(-?[\d.]+)""") | |
| return regex.find(json)?.groupValues?.get(1)?.toFloatOrNull() ?: 0f | |
| } | |
| private fun extractStringArray(json: String, key: String): List<String> { | |
| val keyMatch = Regex(""""$key"\s*:\s*\[""").find(json) ?: return emptyList() | |
| val arrayStart = keyMatch.range.last + 1 | |
| var depth = 1; var pos = arrayStart | |
| while (pos < json.length && depth > 0) { | |
| when (json[pos]) { '[' -> depth++; ']' -> depth-- }; pos++ | |
| } | |
| if (depth != 0) return emptyList() | |
| val arrayContent = json.substring(arrayStart, pos - 1).trim() | |
| if (arrayContent.isEmpty()) return emptyList() | |
| return Regex(""""([^"]*)"""").findAll(arrayContent).map { it.groupValues[1] }.toList() | |
| } |
🧰 Tools
🪛 detekt (1.23.8)
[warning] 79-79: This else block is empty and can be removed.
(detekt.empty-blocks.EmptyElseBlock)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@sdk/runanywhere-kotlin/src/jvmAndroidMain/kotlin/com/runanywhere/sdk/foundation/bridge/extensions/CppBridgeLoraRegistry.kt`
around lines 73 - 118, The JSON parsing is broken because extractString uses a
naive pattern that fails on escaped quotes and
parseLoraEntryArrayJson/extractStringArray treat braces/brackets inside quoted
strings as structural; update extractString to use the same robust string regex
as CppBridgeLLM (pattern like (?:[^"\\]|\\.)* so escaped quotes are handled),
and make parseLoraEntryArrayJson and extractStringArray iterate
character-by-character while tracking whether the parser is currently inside a
quoted string (and skip structural depth changes when inside quotes) and also
correctly handle escaped quote characters; modify the functions named
parseLoraEntryArrayJson, extractStringArray and extractString accordingly so
they ignore structural characters inside quoted strings and accept escaped
characters in string values.
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (5)
examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/MainActivity.kt (1)
61-81: Usewhen (val state = initState)to get safe smart casts and compiler-enforced exhaustiveness.
initStateis a delegated property (by collectAsState()). Kotlin's smart-cast analysis does not apply to delegated properties because each read callsgetValueon the delegate — so on line 67,initStateis accessed again after theis SDKInitializationState.Errorguard, and the explicitascast is technically a TOCTOU. While Compose's single-thread recomposition model makes a crash extremely unlikely today, the pattern is fragile and non-idiomatic.Additionally, because
whenis used as a statement, the compiler does not enforce exhaustiveness for the sealed class — a new subtype would silently fall through.♻️ Proposed fix: capture state in local `val` and use `when` as expression
- when (initState) { - is SDKInitializationState.Loading -> { - InitializationLoadingView() - } - - is SDKInitializationState.Error -> { - val error = (initState as SDKInitializationState.Error).error - InitializationErrorView( - error = error, - onRetry = { - scope.launch { - app.retryInitialization() - } - }, - ) - } - - is SDKInitializationState.Ready -> { - Log.i("MainActivity", "App is ready to use!") - AppNavigation() - } - } + when (val state = initState) { + is SDKInitializationState.Loading -> InitializationLoadingView() + + is SDKInitializationState.Error -> InitializationErrorView( + error = state.error, // safe smart cast on local val + onRetry = { + scope.launch { app.retryInitialization() } + }, + ) + + is SDKInitializationState.Ready -> AppNavigation() + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/MainActivity.kt` around lines 61 - 81, Capture the delegated state into a local val and switch on that local to enable safe smart-casts and exhaustiveness: replace "when (initState)" with "when (val state = initState)" and update branches to reference state (e.g., use state as SDKInitializationState.Error to access state.error and remove the explicit as cast); also use the when as an expression (return/assign its result) so the compiler enforces sealed-class exhaustiveness; ensure calls like InitializationErrorView(onRetry = { scope.launch { app.retryInitialization() } }), InitializationLoadingView(), and AppNavigation() use the new state variable where needed.sdk/runanywhere-kotlin/src/commonMain/kotlin/com/runanywhere/sdk/public/extensions/RunAnywhere+ModelManagement.kt (1)
42-54: KDoc does not document the newsupportsLoraparameter.The
@paramlist documentssupportsThinking(Line 42) but omitssupportsLora. Add a@param supportsLoraentry for API completeness.📝 Proposed doc fix
* `@param` supportsThinking Whether the model supports reasoning/thinking + * `@param` supportsLora Whether the model supports LoRA adapter fine-tuning * `@return` The created ModelInfo */🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-kotlin/src/commonMain/kotlin/com/runanywhere/sdk/public/extensions/RunAnywhere`+ModelManagement.kt around lines 42 - 54, Update the KDoc for the RunAnywhere.registerModel function to include a `@param` entry for the new supportsLora parameter: add a line documenting "@param supportsLora Whether the model supports LoRA/adapter fine-tuning (defaults to false)" alongside the existing `@param` entries so the public API docs reflect the parameter; locate the KDoc above the registerModel declaration to insert this new `@param` line.examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/lora/LoraManagerScreen.kt (1)
101-101: Locale-sensitiveString.formatmay produce unexpected decimal separators.Kotlin's
"%.2f".format(...)(line 101) and"%.1f MB".format(...)(line 258) use the default locale. On devices with locales that use commas as decimal separators (e.g., German), output would be"Scale: 1,00"and"23,5 MB". If you want consistent dot-separated output, useString.format(Locale.US, ...).Also applies to: 258-258
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/lora/LoraManagerScreen.kt` at line 101, The formatted decimal strings use the default locale which can yield commas in some locales; update both format calls to use an explicit locale (Locale.US) to ensure dot separators—replace the use of `"%.2f".format(adapter.scale)` and `"%.1f MB".format(...)` with `String.format(Locale.US, ...)` equivalents so the scale display (the string containing adapter.scale/applied in LoraManagerScreen) and the MB size formatting always use a US-style decimal point.sdk/runanywhere-commons/src/core/rac_core.cpp (1)
39-41: LoRA registry is not cleaned up inrac_shutdown()— same as model registry.Neither
s_model_registrynor the news_lora_registryare destroyed/freed duringrac_shutdown(). This means both registries leak if the library is shut down and re-initialized. This is a pre-existing pattern, not introduced by this PR, but worth tracking.Also applies to: 125-144
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-commons/src/core/rac_core.cpp` around lines 39 - 41, rac_shutdown() currently doesn't free the global registries, causing leaks; update rac_shutdown() to properly destroy both s_model_registry and s_lora_registry: for each registry (s_model_registry and s_lora_registry) lock the corresponding mutex (s_model_registry_mutex and s_lora_registry_mutex), if the handle is non-null call the appropriate destroy/free function for that registry type, set the handle to nullptr, and then unlock; ensure the destroy calls use the existing registry destroy API (e.g., rac_model_registry_destroy / rac_lora_registry_destroy or the appropriate cleanup functions) so repeated init/shutdown cycles don't leak.sdk/runanywhere-kotlin/src/jvmAndroidMain/kotlin/com/runanywhere/sdk/native/bridge/RunAnywhereBridge.kt (1)
518-531: KDoc forracModelRegistrySaveis missing@param supportsLoraThe existing doc block (lines 500–516) documents every parameter individually but omits the newly added
supportsLora: Boolean.📝 Suggested doc update
* `@param` supportsThinking Whether model supports thinking mode + * `@param` supportsLora Whether model supports LoRA adapters * `@param` description Model description (nullable)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-kotlin/src/jvmAndroidMain/kotlin/com/runanywhere/sdk/native/bridge/RunAnywhereBridge.kt` around lines 518 - 531, The KDoc for the external function racModelRegistrySave is missing a `@param` entry for the newly added supportsLora: Boolean parameter; update the existing KDoc for racModelRegistrySave to add a `@param supportsLora` line describing what true/false means (e.g., whether the model supports LoRA adapters), keeping style consistent with the other parameter docs and placing it alongside the other `@param` annotations in the racModelRegistrySave documentation block.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/MainActivity.kt`:
- Line 79: Log.i("MainActivity", "App is ready to use!") is being called
directly inside a composable and will run on every recomposition; move this
logging into a side-effect so it runs only once when the ready state changes.
Wrap the Log.i call in a LaunchedEffect keyed on the readiness state (or
LaunchedEffect(Unit) if it must run once) inside the same composable (e.g., the
MainActivity composable or the Ready composable) so the log executes only when
the state becomes ready; alternatively remove the log if it was only for
development. Use the existing Log.i invocation and replace its direct call with
a LaunchedEffect that calls Log.i to avoid log flooding during recompositions.
In
`@examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/chat/ChatScreen.kt`:
- Around line 646-651: The footer is reserving icon padding even when no model
icon exists because AnalyticsFooter is not informed about message.modelInfo;
update the calls (at the shown block and the other occurrence around lines
935–951) to pass a boolean like hasModelIcon = message.modelInfo != null (or add
a parameter such as showIconPadding) and then update AnalyticsFooter to
conditionally add the start padding only when that flag is true so
timestamps/analytics don't shift right when modelInfo is null.
In
`@examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/chat/ChatViewModel.kt`:
- Around line 803-813: The refreshLoraState() method can start multiple
concurrent coroutines that race and produce stale hasActiveLoraAdapter;
introduce a cancellable Job field (e.g., private var refreshLoraJob: Job?) in
the ViewModel and cancel it before launching a new coroutine, then assign the
newly launched coroutine to refreshLoraJob so any in-flight refresh is cancelled
when refreshLoraState() is called again; update references in refreshLoraState()
to use this job cancellation pattern and keep existing error logging and
UI-state update logic.
In
`@sdk/runanywhere-kotlin/src/jvmAndroidMain/kotlin/com/runanywhere/sdk/foundation/bridge/extensions/CppBridgeLoraRegistry.kt`:
- Around line 27-40: The register function in CppBridgeLoraRegistry.kt currently
throws a generic RuntimeException on failure; update it to throw the
SDKError.llm(...) sealed error instead to match SDK error handling. Locate the
register(entry: LoraEntry) function and replace the RuntimeException thrown when
result != RunAnywhereBridge.RAC_SUCCESS with SDKError.llm(message = "Failed to
register LoRA adapter: ${entry.id}", code = result) (or the SDKError.llm variant
used elsewhere like in CppBridgeLLM), and ensure the process still logs the
error via log(LogLevel.ERROR, ...) before throwing.
---
Duplicate comments:
In
`@examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/data/ModelList.kt`:
- Around line 128-137: This entry (LoraAdapterCatalogEntry with id
"uncensored-chat-lora" in ModelList.kt) ships a LoRA that removes safety
guardrails; either remove this entry entirely from production builds or guard
its inclusion behind a debug-only flag (e.g., wrap the creation/registration of
the "uncensored-chat-lora" entry so it only runs when BuildConfig.DEBUG is
true), ensuring the adapter is not included in release builds or published APKs.
- Around line 162-212: setupModels() calls RunAnywhere.registerModel,
registerMultiFileModel and registerLoraAdapter without catching exceptions, so
any thrown error bubbles up to registerModulesAndModels() and leaves
_initializationState stuck in Loading; wrap the entire body of setupModels() in
a try-catch (or use Kotlin runCatching) to catch Throwable, log the error with
context (include model id/name and which call failed) and surface a controlled
failure path (e.g., rethrow a specific InitializationException or call a
provided failure callback) so registerModulesAndModels() can set
_initializationState to an error/failed state; reference the setupModels
function and the calls RunAnywhere.registerModel,
RunAnywhere.registerMultiFileModel, RunAnywhere.registerLoraAdapter and
registerModulesAndModels to locate where to add the try-catch and logging.
- Around line 87-137: The LoRA entries in loraAdapters are pointing to
Void2377/Qwen (see downloadUrl/filename in each LoraAdapterCatalogEntry) but
compatibleModelIds lists LFM2 IDs, which are architecturally incompatible; fix
by either replacing the compatibleModelIds values with the correct Qwen base
model IDs that these adapters were trained for (update each
LoraAdapterCatalogEntry.compatibleModelIds to the appropriate Qwen model IDs)
or, if you truly intend LFM2 adapters, swap the downloadUrl/filename to the
corresponding LFM2-trained adapter artifacts so downloadUrl, filename, and
compatibleModelIds match.
In
`@examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/chat/ChatViewModel.kt`:
- Around line 115-139: In handleLLMEvent, add a branch for the
model-unload/model-switch event (e.g., LLMEvent.LLMEventType.MODEL_UNLOADED or
the equivalent) that clears both currentModelSupportsLora and
hasActiveLoraAdapter (set to false) so the LoRA UI indicator is reset on model
lifecycle transitions; if no explicit MODEL_UNLOADED event exists, detect the
appropriate event used for model switches and clear those flags there (update
the when block inside handleLLMEvent to include this new arm referencing
currentModelSupportsLora and hasActiveLoraAdapter).
In
`@examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/lora/LoraViewModel.kt`:
- Around line 230-251: The deleteAdapter function performs blocking JNI and file
I/O (calls like RunAnywhere.removeLoraAdapter, file.delete(), and
RunAnywhere.getLoadedLoraAdapters) on the main dispatcher; move those operations
off the main thread by wrapping the try block body in
withContext(Dispatchers.IO) so the blocking calls run on the IO dispatcher while
keeping coroutine scope (viewModelScope.launch) and the surrounding try/catch
and UI-state updates intact.
- Around line 191-201: In LoraViewModel's download coroutine loop (the while
reading bytes into buffer) insert a cancellation check by calling ensureActive()
each iteration so the coroutine can stop promptly when cancelDownload() is
invoked; after the loop, replace the unchecked tmpFile.renameTo(destFile) with a
validated move step that checks the boolean result and handles failure (e.g.,
delete tmpFile, set an error state on _uiState, log the failure or throw an
IOException) so the UI isn't updated to success when the file wasn't moved.
- Around line 93-135: The JNI calls in loadAdapter, unloadAdapter, and clearAll
are running on Dispatchers.Main via viewModelScope.launch; wrap the blocking JNI
calls (RunAnywhere.loadLoraAdapter, removeLoraAdapter, clearLoraAdapters,
getLoadedLoraAdapters) inside withContext(Dispatchers.IO) to move them off the
main thread, then switch back to the main dispatcher (or simply update _uiState
after the withContext block) to update UI state and logging; locate these
changes in the functions loadAdapter, unloadAdapter, and clearAll and ensure
exception handling remains intact while the heavy JNI calls run on
Dispatchers.IO.
In `@sdk/runanywhere-commons/include/rac/core/rac_core.h`:
- Around line 338-346: The header declares and exposes
rac_lora_registry/rac_register_lora/rac_get_lora_for_model but does not include
the full definition of struct rac_lora_entry, preventing callers from
constructing or dereferencing entries; add the appropriate include (the header
that defines rac_lora_entry — e.g., the lora types header used analogously to
rac_model_types.h) at the top of this file so rac_lora_entry is defined before
declaring rac_register_lora and rac_get_lora_for_model.
- Around line 342-346: Add Doxygen comments for the three new public
APIs—rac_get_lora_registry, rac_register_lora, and
rac_get_lora_for_model—matching the style of other public C APIs: include a
brief description, `@param` entries for every argument, explicit `@return`
documenting all rac_result_t error codes, and clear ownership/lifecycle
semantics (who allocates/deallocates memory, e.g., whether
rac_get_lora_for_model allocates the array and entries and how callers must free
them, or if they point into registry memory). Also document any
vtable/registration invariants for rac_register_lora (e.g., when entries must
remain valid), and note thread-safety expectations for rac_get_lora_registry.
In `@sdk/runanywhere-commons/include/rac/features/llm/rac_llm_component.h`:
- Around line 266-279: The docblock for rac_llm_component_check_lora_compat
misstates behavior and omits error codes; update the comment for
rac_llm_component_check_lora_compat to reflect actual checks (verifies handle
validity, non-empty adapter_path, model loaded, and backend vtable exposes
load_lora) instead of checking file existence, and enumerate the possible return
values (RAC_ERROR_INVALID_HANDLE, RAC_ERROR_INVALID_ARGUMENT,
RAC_ERROR_COMPONENT_NOT_READY, RAC_ERROR_NOT_SUPPORTED, RAC_SUCCESS) plus caller
responsibility to free out_error with rac_free and any lifecycle/vtable
requirements for the backend’s load_lora operation.
In
`@sdk/runanywhere-commons/include/rac/infrastructure/model_management/rac_lora_registry.h`:
- Around line 42-67: Add per-function API contracts for each declared function
(rac_lora_registry_create, rac_lora_registry_destroy,
rac_lora_registry_register, rac_lora_registry_remove, rac_lora_registry_get_all,
rac_lora_registry_get_for_model, rac_lora_registry_get, rac_lora_entry_free,
rac_lora_entry_array_free, rac_lora_entry_copy) documenting: allowed
rac_result_t return values and when they are returned, precise
ownership/lifecycle rules (who allocates/frees memory — e.g., caller must free
entries returned by rac_lora_registry_get_all and
rac_lora_registry_get_for_model via rac_lora_entry_array_free,
rac_lora_entry_free for single entry, and that rac_lora_entry_copy returns a
heap-owned copy), and NULL-safety/precondition behavior (which parameters may be
NULL and which will cause RAC_RESULT_INVALID_ARGUMENT), plus any thread-safety
or vtable expectations; add these as brief comment blocks directly above each
function declaration to satisfy the include/rac/ public C API guidelines.
In `@sdk/runanywhere-commons/src/features/llm/llm_component.cpp`:
- Around line 885-889: Move the pure argument validation for adapter_path to
before the mutex acquisition and service readiness check: check if
strlen(adapter_path) == 0 first, set *out_error = strdup("Empty adapter path")
and return RAC_ERROR_INVALID_ARGUMENT immediately (instead of performing the
lock/service lookup that may return RAC_ERROR_COMPONENT_NOT_READY); update the
code paths around the existing mutex/service-check block (where the lock is
taken and the model/service is validated) so they only run after the
adapter_path validation passes.
In
`@sdk/runanywhere-commons/src/infrastructure/model_management/lora_registry.cpp`:
- Around line 19-21: The map in rac_lora_registry uses raw std::string keys
(entries) which violates the structured-types rule; introduce a small strong
identifier type (e.g., struct AdapterId or using AdapterId = /*strong typedef*/
) and change entries to std::map<AdapterId, rac_lora_entry_t*> (or
std::unordered_map with hash) and implement the required comparison/hash and
conversion utilities; update all references/lookups/insertions that use the
string key to use the new AdapterId type (and adjust rac_lora_entry_t if it
embeds the raw id) so the registry consistently uses the structured identifier
instead of std::string.
- Around line 24-44: deep_copy_lora_entry currently returns a
partially-initialized rac_lora_entry_t on OOM; update deep_copy_lora_entry to
check every allocation (rac_strdup and malloc for compatible_model_ids) and, on
any failure, free all previously-allocated strings/arrays and the copy struct
and return nullptr to avoid leaks/partial objects; ensure you validate
rac_strdup for id/name/description/download_url/filename and each
compatible_model_ids[i], free compatible_model_ids array only if allocated, and
only set compatible_model_count after all items succeed.
In `@sdk/runanywhere-commons/src/jni/runanywhere_commons_jni.cpp`:
- Around line 1953-1970: The current loraEntryToJson(const rac_lora_entry_t*
entry) returns a JSON string (stringly-typed) which violates structured-types
guidance; replace it with a function that builds and returns a typed JNI object
(e.g., jobject) representing a LoRA entry (or a native structured container)
instead of a string. Implement a new function, e.g.,
createLoRAEntryObject(JNIEnv* env, const rac_lora_entry_t* entry), that maps
fields from rac_lora_entry_t (id, name, description, download_url, filename,
file_size, default_scale, compatible_model_ids) into a Java LoRAEntry class or
equivalent structured JNI array/list for compatible_model_ids, handle nulls as
empty strings, and update all callers of loraEntryToJson to use the new creator
so callers receive a typed object rather than a JSON string.
- Around line 1212-1328: Add JNI wrappers for rac_lora_registry_get and
rac_lora_registry_remove similar to the existing register/get_all/get_for_model
functions: implement
Java_com_runanywhere_sdk_native_bridge_RunAnywhereBridge_racLoraRegistryGet(JNIEnv*,
jclass, jstring modelId) that checks modelId, acquires id_str via
GetStringUTFChars, calls rac_lora_registry_get(rac_get_lora_registry(), id_str,
&entry) (or the exact C API signature), converts the returned rac_lora_entry_t*
to JSON using the existing loraEntryToJson helper, releases the UTF chars, frees
any C API-owned memory via rac_lora_entry_free if needed, and returns
NewStringUTF(json.c_str()) (or "null"/"[]" on errors); and implement
Java_com_runanywhere_sdk_native_bridge_RunAnywhereBridge_racLoraRegistryRemove(JNIEnv*,
jclass, jstring id) that validates id, obtains id_str, calls
rac_lora_registry_remove(rac_get_lora_registry(), id_str), releases the UTF
chars, logs the result consistent with other JNI functions and returns the
rac_result_t as jint. Ensure you follow the same error checks, logging
(LOGi/LOGe), and memory release patterns used around racLoraRegistryRegister /
racLoraRegistryGetAll to avoid leaks.
In
`@sdk/runanywhere-kotlin/src/commonMain/kotlin/com/runanywhere/sdk/public/extensions/Models/ModelTypes.kt`:
- Around line 347-349: The Kotlin data model includes a supportsLora Boolean
that does not exist in the Swift ModelInfo (violating the “mirrors Swift
exactly” claim); either remove supportsLora to match the Swift source of truth
(and update any usages of supportsLora in the codebase), or if LoRA support is
intentionally Android-first keep the field but change the file header comment to
no longer assert an exact mirror and add a short inline comment by supportsLora
explaining the intentional divergence; ensure any serialization, defaults, and
consumers of supportsLora (references to supportsLora) are updated accordingly.
In
`@sdk/runanywhere-kotlin/src/jvmAndroidMain/kotlin/com/runanywhere/sdk/foundation/bridge/extensions/CppBridgeLoraRegistry.kt`:
- Around line 92-95: The extractString function's regex in CppBridgeLoraRegistry
(private fun extractString) is too naive and fails on escaped quotes; replace
the pattern """"$key"\s*:\s*"([^"]*)"""" with one that accepts escaped
characters like """"$key"\s*:\s*"(?:[^"\\]|\\.)*"""" and capture the inner group
(use the same (?:[^"\\]|\\.)* approach as in CppBridgeLLM.kt), leaving the rest
of the method logic (regex.find(...).groupValues[1].takeIf { it.isNotEmpty() })
unchanged.
- Around line 73-90: The brace-depth parsing in parseLoraEntryArrayJson fails to
ignore braces inside JSON strings, so update parseLoraEntryArrayJson to track
quoted-string context and escape sequences (e.g., maintain an inString boolean
toggled by '"' while respecting backslash escapes) and only increment/decrement
depth for '{' and '}' when not inside a string; apply the same approach in
extractStringArray (which currently tracks bracket depth) so brackets inside
strings are ignored, and ensure objectStart/arrayStart logic is only triggered
when not inString to correctly identify element boundaries.
In
`@sdk/runanywhere-kotlin/src/jvmAndroidMain/kotlin/com/runanywhere/sdk/public/extensions/RunAnywhere`+LoRA.jvmAndroid.kt:
- Around line 100-121: These three functions (registerLoraAdapter,
loraAdaptersForModel, allRegisteredLoraAdapters) call into CppBridgeLoraRegistry
without checking isInitialized; add the same initialization guard used
elsewhere: if !isInitialized throw SDKError.notInitialized, before any
CppBridgeLoraRegistry calls, so JNI is never touched when the native lib isn't
loaded; keep the rest of the implementation unchanged and reference the
isInitialized flag and SDKError.notInitialized to match the pattern used by
other methods in this file.
---
Nitpick comments:
In
`@examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/MainActivity.kt`:
- Around line 61-81: Capture the delegated state into a local val and switch on
that local to enable safe smart-casts and exhaustiveness: replace "when
(initState)" with "when (val state = initState)" and update branches to
reference state (e.g., use state as SDKInitializationState.Error to access
state.error and remove the explicit as cast); also use the when as an expression
(return/assign its result) so the compiler enforces sealed-class exhaustiveness;
ensure calls like InitializationErrorView(onRetry = { scope.launch {
app.retryInitialization() } }), InitializationLoadingView(), and AppNavigation()
use the new state variable where needed.
In
`@examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/lora/LoraManagerScreen.kt`:
- Line 101: The formatted decimal strings use the default locale which can yield
commas in some locales; update both format calls to use an explicit locale
(Locale.US) to ensure dot separators—replace the use of
`"%.2f".format(adapter.scale)` and `"%.1f MB".format(...)` with
`String.format(Locale.US, ...)` equivalents so the scale display (the string
containing adapter.scale/applied in LoraManagerScreen) and the MB size
formatting always use a US-style decimal point.
In `@sdk/runanywhere-commons/src/core/rac_core.cpp`:
- Around line 39-41: rac_shutdown() currently doesn't free the global
registries, causing leaks; update rac_shutdown() to properly destroy both
s_model_registry and s_lora_registry: for each registry (s_model_registry and
s_lora_registry) lock the corresponding mutex (s_model_registry_mutex and
s_lora_registry_mutex), if the handle is non-null call the appropriate
destroy/free function for that registry type, set the handle to nullptr, and
then unlock; ensure the destroy calls use the existing registry destroy API
(e.g., rac_model_registry_destroy / rac_lora_registry_destroy or the appropriate
cleanup functions) so repeated init/shutdown cycles don't leak.
In
`@sdk/runanywhere-kotlin/src/commonMain/kotlin/com/runanywhere/sdk/public/extensions/RunAnywhere`+ModelManagement.kt:
- Around line 42-54: Update the KDoc for the RunAnywhere.registerModel function
to include a `@param` entry for the new supportsLora parameter: add a line
documenting "@param supportsLora Whether the model supports LoRA/adapter
fine-tuning (defaults to false)" alongside the existing `@param` entries so the
public API docs reflect the parameter; locate the KDoc above the registerModel
declaration to insert this new `@param` line.
In
`@sdk/runanywhere-kotlin/src/jvmAndroidMain/kotlin/com/runanywhere/sdk/native/bridge/RunAnywhereBridge.kt`:
- Around line 518-531: The KDoc for the external function racModelRegistrySave
is missing a `@param` entry for the newly added supportsLora: Boolean parameter;
update the existing KDoc for racModelRegistrySave to add a `@param supportsLora`
line describing what true/false means (e.g., whether the model supports LoRA
adapters), keeping style consistent with the other parameter docs and placing it
alongside the other `@param` annotations in the racModelRegistrySave documentation
block.
| AppNavigation() | ||
| } | ||
| is SDKInitializationState.Ready -> { | ||
| Log.i("MainActivity", "App is ready to use!") |
There was a problem hiding this comment.
Log.i inside a composable body fires on every recomposition.
Any composable may recompose many times. A bare Log.i call here will flood logcat whenever the Ready state is current. Side effects in Compose should be confined to LaunchedEffect (or removed entirely if the log is only for development):
🛠️ Proposed fix
is SDKInitializationState.Ready -> {
- Log.i("MainActivity", "App is ready to use!")
AppNavigation()
}Or, if the log is intentionally kept:
is SDKInitializationState.Ready -> {
+ LaunchedEffect(Unit) {
+ Log.i("MainActivity", "App is ready to use!")
+ }
AppNavigation()
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| Log.i("MainActivity", "App is ready to use!") | |
| AppNavigation() |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/MainActivity.kt`
at line 79, Log.i("MainActivity", "App is ready to use!") is being called
directly inside a composable and will run on every recomposition; move this
logging into a side-effect so it runs only once when the ready state changes.
Wrap the Log.i call in a LaunchedEffect keyed on the readiness state (or
LaunchedEffect(Unit) if it must run once) inside the same composable (e.g., the
MainActivity composable or the Ready composable) so the log executes only when
the state becomes ready; alternatively remove the log if it was only for
development. Use the existing Log.i invocation and replace its direct call with
a LaunchedEffect that calls Log.i to avoid log flooding during recompositions.
| AnalyticsFooter( | ||
| message = message, | ||
| hasThinking = message.thinkingContent != null, | ||
| alignEnd = false, | ||
| ) | ||
| } |
There was a problem hiding this comment.
Analytics footer padding should depend on icon presence.
When message.modelInfo is null, the footer still reserves icon padding, so the timestamp/analytics shift right unnecessarily.
🩹 Suggested tweak
- AnalyticsFooter(
- message = message,
- hasThinking = message.thinkingContent != null,
- alignEnd = false,
- )
+ AnalyticsFooter(
+ message = message,
+ hasThinking = message.thinkingContent != null,
+ alignEnd = false,
+ leadingPadding = if (message.modelInfo != null) {
+ Dimensions.assistantIconSize + Dimensions.assistantIconSpacing
+ } else {
+ 0.dp
+ },
+ ) fun AnalyticsFooter(
message: ChatMessage,
hasThinking: Boolean,
- alignEnd: Boolean = true,
+ alignEnd: Boolean = true,
+ leadingPadding: Dp = 0.dp,
) {
@@
- modifier = if (alignEnd) Modifier.padding(start = Dimensions.mediumLarge) else Modifier.padding(start = Dimensions.assistantIconSize + Dimensions.assistantIconSpacing),
+ modifier = if (alignEnd) Modifier.padding(start = Dimensions.mediumLarge) else Modifier.padding(start = leadingPadding),
) {Also applies to: 935-951
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/chat/ChatScreen.kt`
around lines 646 - 651, The footer is reserving icon padding even when no model
icon exists because AnalyticsFooter is not informed about message.modelInfo;
update the calls (at the shown block and the other occurrence around lines
935–951) to pass a boolean like hasModelIcon = message.modelInfo != null (or add
a parameter such as showIconPadding) and then update AnalyticsFooter to
conditionally add the start padding only when that flag is true so
timestamps/analytics don't shift right when modelInfo is null.
| /** Refresh LoRA loaded state for the active adapters indicator. */ | ||
| fun refreshLoraState() { | ||
| viewModelScope.launch { | ||
| try { | ||
| val loaded = RunAnywhere.getLoadedLoraAdapters() | ||
| _uiState.value = _uiState.value.copy(hasActiveLoraAdapter = loaded.isNotEmpty()) | ||
| } catch (e: Exception) { | ||
| Log.e(TAG, "Failed to refresh LoRA state", e) | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Concurrent calls to refreshLoraState() can write stale state.
Every invocation fires a new independent coroutine. If the user loads and immediately unloads an adapter in quick succession, two coroutines race and whichever finishes last wins — potentially leaving hasActiveLoraAdapter set to the wrong value.
Cancel any in-flight refresh before launching a new one:
🔒 Proposed fix
+ private var loraStateJob: Job? = null
+
/** Refresh LoRA loaded state for the active adapters indicator. */
fun refreshLoraState() {
- viewModelScope.launch {
+ loraStateJob?.cancel()
+ loraStateJob = viewModelScope.launch {
try {
val loaded = RunAnywhere.getLoadedLoraAdapters()
_uiState.value = _uiState.value.copy(hasActiveLoraAdapter = loaded.isNotEmpty())
} catch (e: Exception) {
Log.e(TAG, "Failed to refresh LoRA state", e)
}
}
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/chat/ChatViewModel.kt`
around lines 803 - 813, The refreshLoraState() method can start multiple
concurrent coroutines that race and produce stale hasActiveLoraAdapter;
introduce a cancellable Job field (e.g., private var refreshLoraJob: Job?) in
the ViewModel and cancel it before launching a new coroutine, then assign the
newly launched coroutine to refreshLoraJob so any in-flight refresh is cancelled
when refreshLoraState() is called again; update references in refreshLoraState()
to use this job cancellation pattern and keep existing error logging and
UI-state update logic.
| fun register(entry: LoraEntry) { | ||
| log(LogLevel.DEBUG, "Registering LoRA adapter: ${entry.id}") | ||
| val result = RunAnywhereBridge.racLoraRegistryRegister( | ||
| id = entry.id, name = entry.name, description = entry.description, | ||
| downloadUrl = entry.downloadUrl, filename = entry.filename, | ||
| compatibleModelIds = entry.compatibleModelIds.toTypedArray(), | ||
| fileSize = entry.fileSize, defaultScale = entry.defaultScale, | ||
| ) | ||
| if (result != RunAnywhereBridge.RAC_SUCCESS) { | ||
| log(LogLevel.ERROR, "Failed to register LoRA adapter: ${entry.id}, error=$result") | ||
| throw RuntimeException("Failed to register LoRA adapter: $result") | ||
| } | ||
| log(LogLevel.INFO, "LoRA adapter registered: ${entry.id}") | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check how other CppBridge files handle errors
rg "throw RuntimeException|throw SDKError" --type=kt -g '*CppBridge*' -n -C1Repository: RunanywhereAI/runanywhere-sdks
Length of output: 100
🏁 Script executed:
#!/bin/bash
# Check if SDKError exists and what factory methods are available
rg "sealed class SDKError|object SDKError|fun llm|fun error" --type=kt -g '*SDKError*' -n | head -30Repository: RunanywhereAI/runanywhere-sdks
Length of output: 100
🏁 Script executed:
#!/bin/bash
# Check other functions in the same file for error handling patterns
cat -n "sdk/runanywhere-kotlin/src/jvmAndroidMain/kotlin/com/runanywhere/sdk/foundation/bridge/extensions/CppBridgeLoraRegistry.kt"Repository: RunanywhereAI/runanywhere-sdks
Length of output: 6177
🏁 Script executed:
#!/bin/bash
# Search for SDKError definition - try without file type
rg "sealed class SDKError|object SDKError|class SDKError" --type-list | head -10Repository: RunanywhereAI/runanywhere-sdks
Length of output: 463
🏁 Script executed:
#!/bin/bash
# Search for SDKError in the codebase
rg "SDKError" -A 5 | grep -E "sealed class|object|fun |import" | head -20Repository: RunanywhereAI/runanywhere-sdks
Length of output: 2717
🏁 Script executed:
#!/bin/bash
# Look for other CppBridge files and how they handle errors
find . -name "*CppBridge*.kt" -type f | head -10Repository: RunanywhereAI/runanywhere-sdks
Length of output: 1260
🏁 Script executed:
#!/bin/bash
# Check how errors are thrown in the codebase broadly
rg "throw.*Error|throw RuntimeException" -A 1 | head -40Repository: RunanywhereAI/runanywhere-sdks
Length of output: 3567
🏁 Script executed:
#!/bin/bash
# Find SDKError definition
find . -name "*SDKError*" -type f | grep kotlinRepository: RunanywhereAI/runanywhere-sdks
Length of output: 170
🏁 Script executed:
#!/bin/bash
# Check a few other CppBridge files for error handling patterns
cat -n "sdk/runanywhere-kotlin/src/jvmAndroidMain/kotlin/com/runanywhere/sdk/foundation/bridge/extensions/CppBridgeAuth.kt" | head -50Repository: RunanywhereAI/runanywhere-sdks
Length of output: 1868
🏁 Script executed:
#!/bin/bash
# Check CppBridgeLLM since it's also an LLM-related component
cat -n "sdk/runanywhere-kotlin/src/jvmAndroidMain/kotlin/com/runanywhere/sdk/foundation/bridge/extensions/CppBridgeLLM.kt" | head -100Repository: RunanywhereAI/runanywhere-sdks
Length of output: 3626
🏁 Script executed:
#!/bin/bash
# Look at SDKError definition - check the errors module
find . -path "*/foundation/errors*" -name "*.kt" -type fRepository: RunanywhereAI/runanywhere-sdks
Length of output: 478
🏁 Script executed:
#!/bin/bash
# Read the SDKError definition
cat -n "sdk/runanywhere-kotlin/src/commonMain/kotlin/com/runanywhere/sdk/foundation/errors/SDKError.kt"Repository: RunanywhereAI/runanywhere-sdks
Length of output: 39337
🏁 Script executed:
#!/bin/bash
# Check for llm error factory method in SDKError
rg "fun llm|\.llm\(" "sdk/runanywhere-kotlin/src/commonMain/kotlin/com/runanywhere/sdk/foundation/errors/" -A 2Repository: RunanywhereAI/runanywhere-sdks
Length of output: 1307
🏁 Script executed:
#!/bin/bash
# Search more broadly for SDKError factory methods
cat -n "sdk/runanywhere-kotlin/src/commonMain/kotlin/com/runanywhere/sdk/foundation/errors/SDKError.kt" | grep -E "fun |object |companion" -A 1Repository: RunanywhereAI/runanywhere-sdks
Length of output: 6182
🏁 Script executed:
#!/bin/bash
# Check if there are other LoRA-related errors or if the model() factory might be more appropriate
cat -n "sdk/runanywhere-kotlin/src/commonMain/kotlin/com/runanywhere/sdk/foundation/errors/SDKError.kt" | grep -A 10 "fun model("Repository: RunanywhereAI/runanywhere-sdks
Length of output: 495
🏁 Script executed:
#!/bin/bash
# Check how CppBridgeLLM uses SDKError to see the pattern
grep -n "throw SDKError" "sdk/runanywhere-kotlin/src/jvmAndroidMain/kotlin/com/runanywhere/sdk/foundation/bridge/extensions/CppBridgeLLM.kt" | head -10Repository: RunanywhereAI/runanywhere-sdks
Length of output: 778
Replace generic RuntimeException with SDKError.llm() for type-safe error handling.
The register function violates the coding guideline requiring sealed classes for error handling. Wrap the failure in SDKError.llm(...) to align with error handling patterns used elsewhere in the SDK (e.g., CppBridgeLLM).
Suggested change
+import com.runanywhere.sdk.foundation.errors.SDKError
+
if (result != RunAnywhereBridge.RAC_SUCCESS) {
log(LogLevel.ERROR, "Failed to register LoRA adapter: ${entry.id}, error=$result")
- throw RuntimeException("Failed to register LoRA adapter: $result")
+ throw SDKError.llm("Failed to register LoRA adapter: ${entry.id}, error=$result")
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| fun register(entry: LoraEntry) { | |
| log(LogLevel.DEBUG, "Registering LoRA adapter: ${entry.id}") | |
| val result = RunAnywhereBridge.racLoraRegistryRegister( | |
| id = entry.id, name = entry.name, description = entry.description, | |
| downloadUrl = entry.downloadUrl, filename = entry.filename, | |
| compatibleModelIds = entry.compatibleModelIds.toTypedArray(), | |
| fileSize = entry.fileSize, defaultScale = entry.defaultScale, | |
| ) | |
| if (result != RunAnywhereBridge.RAC_SUCCESS) { | |
| log(LogLevel.ERROR, "Failed to register LoRA adapter: ${entry.id}, error=$result") | |
| throw RuntimeException("Failed to register LoRA adapter: $result") | |
| } | |
| log(LogLevel.INFO, "LoRA adapter registered: ${entry.id}") | |
| } | |
| import com.runanywhere.sdk.foundation.errors.SDKError | |
| fun register(entry: LoraEntry) { | |
| log(LogLevel.DEBUG, "Registering LoRA adapter: ${entry.id}") | |
| val result = RunAnywhereBridge.racLoraRegistryRegister( | |
| id = entry.id, name = entry.name, description = entry.description, | |
| downloadUrl = entry.downloadUrl, filename = entry.filename, | |
| compatibleModelIds = entry.compatibleModelIds.toTypedArray(), | |
| fileSize = entry.fileSize, defaultScale = entry.defaultScale, | |
| ) | |
| if (result != RunAnywhereBridge.RAC_SUCCESS) { | |
| log(LogLevel.ERROR, "Failed to register LoRA adapter: ${entry.id}, error=$result") | |
| throw SDKError.llm("Failed to register LoRA adapter: ${entry.id}, error=$result") | |
| } | |
| log(LogLevel.INFO, "LoRA adapter registered: ${entry.id}") | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@sdk/runanywhere-kotlin/src/jvmAndroidMain/kotlin/com/runanywhere/sdk/foundation/bridge/extensions/CppBridgeLoraRegistry.kt`
around lines 27 - 40, The register function in CppBridgeLoraRegistry.kt
currently throws a generic RuntimeException on failure; update it to throw the
SDKError.llm(...) sealed error instead to match SDK error handling. Locate the
register(entry: LoraEntry) function and replace the RuntimeException thrown when
result != RunAnywhereBridge.RAC_SUCCESS with SDKError.llm(message = "Failed to
register LoRA adapter: ${entry.id}", code = result) (or the SDKError.llm variant
used elsewhere like in CppBridgeLLM), and ensure the process still logs the
error via log(LogLevel.ERROR, ...) before throwing.
- Remove uncensored-chat LoRA adapter (app store policy) - Move all SDK/JNI calls off main thread with Dispatchers.IO - Add per-model error handling in setupModels() so one failure doesn't block others - Fix tmp file cleanup on download cancellation - Fix TOCTOU race in deleteAdapter by always attempting unload - Check renameTo() return value in download flow - Add key to remember block for scale state in picker
…d API fixes Scanned by claude-code, written by Siddhesh. - Add TopBarState with ConfigureTopBar/ConfigureCustomTopBar for dynamic top bar - Migrate all screens from AppScreenScaffold to root Scaffold in AppNavigation - Delete unused AppScreenScaffold.kt - Migrate deprecated ClickableText to Text with LinkAnnotation - Migrate deprecated LocalClipboardManager to LocalClipboard + ClipEntry - Replace all deprecated Icons with AutoMirrored variants - Remove unused @OptIn annotations and imports - Fix indentation inconsistencies from refactoring - Add imePadding to chat input Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…inline LoRA badge
|
@coderabbitai please do a detailed review. Thanks! |
|
@greptile please do a detailed review. Thanks! |
|
Tip For best results, initiate chat on the files or code changes.
🏗️ Architecture & Design✅ Strengths
|
| private init() { | ||
| var h: rac_lora_registry_handle_t? | ||
| let result = rac_lora_registry_create(&h) | ||
| if result == RAC_SUCCESS { | ||
| handle = h | ||
| logger.debug("LoRA registry created") | ||
| } else { | ||
| logger.error("Failed to create LoRA registry: \(result)") | ||
| } |
There was a problem hiding this comment.
Swift LoRA registry is separate from the global C++ registry
The Swift LoraRegistry actor creates its own C++ registry instance via rac_lora_registry_create() (line 25). However, the Kotlin/Android SDK uses rac_register_lora() and rac_get_lora_for_model() from rac_core.h, which operate on the global singleton registry created by rac_get_lora_registry() in rac_core.cpp.
This means the two platforms use structurally different registries. While this isn't a runtime bug (they run on different platforms), it's an architectural inconsistency that could cause confusion and diverge over time. If a future feature ever needs to share registry state (e.g., cross-platform sync or pre-populated C++ defaults), this mismatch would cause silent data loss.
Consider using the global convenience functions (rac_register_lora, rac_get_lora_for_model) from rac_core.h on the Swift side as well, matching the Kotlin pattern.
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Prompt To Fix With AI
This is a comment left during a code review.
Path: sdk/runanywhere-swift/Sources/RunAnywhere/Foundation/Bridge/Extensions/CppBridge+LoraRegistry.swift
Line: 23-31
Comment:
**Swift LoRA registry is separate from the global C++ registry**
The Swift `LoraRegistry` actor creates its own C++ registry instance via `rac_lora_registry_create()` (line 25). However, the Kotlin/Android SDK uses `rac_register_lora()` and `rac_get_lora_for_model()` from `rac_core.h`, which operate on the **global** singleton registry created by `rac_get_lora_registry()` in `rac_core.cpp`.
This means the two platforms use structurally different registries. While this isn't a runtime bug (they run on different platforms), it's an architectural inconsistency that could cause confusion and diverge over time. If a future feature ever needs to share registry state (e.g., cross-platform sync or pre-populated C++ defaults), this mismatch would cause silent data loss.
Consider using the global convenience functions (`rac_register_lora`, `rac_get_lora_for_model`) from `rac_core.h` on the Swift side as well, matching the Kotlin pattern.
<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>
How can I resolve this? If you propose a fix, please make it concise.| downloadUrl = "https://huggingface.co/Void2377/Qwen/resolve/main/lora/summarizer-lora-Q8_0.gguf", | ||
| filename = "summarizer-lora-Q8_0.gguf", | ||
| compatibleModelIds = listOf("lfm2-350m-q4_k_m", "lfm2-350m-q8_0"), | ||
| fileSize = 690_176, | ||
| defaultScale = 1.0f, | ||
| ), | ||
| LoraAdapterCatalogEntry( | ||
| id = "translator-lora", | ||
| name = "Translator", | ||
| description = "Improves translation between languages", | ||
| downloadUrl = "https://huggingface.co/Void2377/Qwen/resolve/main/lora/translator-lora-Q8_0.gguf", | ||
| filename = "translator-lora-Q8_0.gguf", | ||
| compatibleModelIds = listOf("lfm2-350m-q4_k_m", "lfm2-350m-q8_0"), | ||
| fileSize = 690_176, | ||
| defaultScale = 1.0f, | ||
| ), | ||
| LoraAdapterCatalogEntry( | ||
| id = "sentiment-lora", | ||
| name = "Sentiment Analysis", | ||
| description = "Fine-tuned for sentiment analysis tasks", | ||
| downloadUrl = "https://huggingface.co/Void2377/Qwen/resolve/main/lora/sentiment-lora-Q8_0.gguf", | ||
| filename = "sentiment-lora-Q8_0.gguf", | ||
| compatibleModelIds = listOf("lfm2-350m-q4_k_m", "lfm2-350m-q8_0"), | ||
| fileSize = 690_176, | ||
| defaultScale = 1.0f, | ||
| ), | ||
| ) | ||
|
|
||
| // VLM | ||
| private val vlmModels = listOf( | ||
| AppModel(id = "smolvlm-500m-instruct-q8_0", name = "SmolVLM 500M Instruct", | ||
| url = "https://github.com/RunanywhereAI/sherpa-onnx/releases/download/runanywhere-vlm-models-v1/smolvlm-500m-instruct-q8_0.tar.gz", |
There was a problem hiding this comment.
LoRA adapters from Void2377/Qwen repo may not be compatible with LFM2-350M
These four LoRA adapter files are hosted under Void2377/Qwen on HuggingFace and are described as Qwen-trained LoRA adapters. They are declared as compatibleModelIds = listOf("lfm2-350m-q4_k_m", "lfm2-350m-q8_0"), mapping them to LiquidAI LFM2 models.
LoRA adapters are architecture-specific — a LoRA trained on Qwen's architecture will not produce correct outputs (and may crash or produce garbage) when applied to a different architecture like LFM2. Unless these files were specifically trained or adapted for LFM2-350M, applying them will likely result in degraded or nonsensical output at best, or a runtime error at worst.
Can you confirm these adapters were actually trained on/for LFM2-350M? If these are placeholder entries for demo purposes, consider adding a comment clarifying that.
Prompt To Fix With AI
This is a comment left during a code review.
Path: examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/data/ModelList.kt
Line: 100-131
Comment:
**LoRA adapters from `Void2377/Qwen` repo may not be compatible with LFM2-350M**
These four LoRA adapter files are hosted under `Void2377/Qwen` on HuggingFace and are described as Qwen-trained LoRA adapters. They are declared as `compatibleModelIds = listOf("lfm2-350m-q4_k_m", "lfm2-350m-q8_0")`, mapping them to LiquidAI LFM2 models.
LoRA adapters are architecture-specific — a LoRA trained on Qwen's architecture will not produce correct outputs (and may crash or produce garbage) when applied to a different architecture like LFM2. Unless these files were specifically trained or adapted for LFM2-350M, applying them will likely result in degraded or nonsensical output at best, or a runtime error at worst.
Can you confirm these adapters were actually trained on/for LFM2-350M? If these are placeholder entries for demo purposes, consider adding a comment clarifying that.
How can I resolve this? If you propose a fix, please make it concise.| try { | ||
| withContext(Dispatchers.IO) { | ||
| val connection = URL(entry.downloadUrl).openConnection().apply { | ||
| connectTimeout = 30_000 | ||
| readTimeout = 60_000 | ||
| } | ||
| connection.connect() | ||
| val totalSize = connection.contentLengthLong.takeIf { it > 0 } ?: entry.fileSize | ||
| var downloaded = 0L | ||
|
|
||
| connection.getInputStream().buffered().use { input -> | ||
| tmpFile.outputStream().buffered().use { output -> | ||
| val buffer = ByteArray(8192) | ||
| var bytesRead: Int | ||
| while (input.read(buffer).also { bytesRead = it } != -1) { | ||
| output.write(buffer, 0, bytesRead) | ||
| downloaded += bytesRead | ||
| if (totalSize > 0) { | ||
| val progress = (downloaded.toFloat() / totalSize).coerceIn(0f, 1f) | ||
| _uiState.value = _uiState.value.copy(downloadProgress = progress) | ||
| } | ||
| } | ||
| } | ||
| } | ||
| if (!tmpFile.renameTo(destFile)) { |
There was a problem hiding this comment.
Download over HTTP is unrestricted — no URL scheme validation
The downloadAdapter function passes the entry.downloadUrl directly to URL(entry.downloadUrl).openConnection() without verifying the scheme is HTTPS. If a catalog entry were registered with an http:// URL (or any other scheme), this would download the adapter over an unencrypted connection, exposing the user to man-in-the-middle attacks that could inject a malicious model file.
Consider adding a scheme check:
| try { | |
| withContext(Dispatchers.IO) { | |
| val connection = URL(entry.downloadUrl).openConnection().apply { | |
| connectTimeout = 30_000 | |
| readTimeout = 60_000 | |
| } | |
| connection.connect() | |
| val totalSize = connection.contentLengthLong.takeIf { it > 0 } ?: entry.fileSize | |
| var downloaded = 0L | |
| connection.getInputStream().buffered().use { input -> | |
| tmpFile.outputStream().buffered().use { output -> | |
| val buffer = ByteArray(8192) | |
| var bytesRead: Int | |
| while (input.read(buffer).also { bytesRead = it } != -1) { | |
| output.write(buffer, 0, bytesRead) | |
| downloaded += bytesRead | |
| if (totalSize > 0) { | |
| val progress = (downloaded.toFloat() / totalSize).coerceIn(0f, 1f) | |
| _uiState.value = _uiState.value.copy(downloadProgress = progress) | |
| } | |
| } | |
| } | |
| } | |
| if (!tmpFile.renameTo(destFile)) { | |
| withContext(Dispatchers.IO) { | |
| val url = URL(entry.downloadUrl) | |
| require(url.protocol == "https") { "Only HTTPS downloads are supported" } | |
| val connection = url.openConnection().apply { |
Prompt To Fix With AI
This is a comment left during a code review.
Path: examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/lora/LoraViewModel.kt
Line: 180-204
Comment:
**Download over HTTP is unrestricted — no URL scheme validation**
The `downloadAdapter` function passes the `entry.downloadUrl` directly to `URL(entry.downloadUrl).openConnection()` without verifying the scheme is HTTPS. If a catalog entry were registered with an `http://` URL (or any other scheme), this would download the adapter over an unencrypted connection, exposing the user to man-in-the-middle attacks that could inject a malicious model file.
Consider adding a scheme check:
```suggestion
withContext(Dispatchers.IO) {
val url = URL(entry.downloadUrl)
require(url.protocol == "https") { "Only HTTPS downloads are supported" }
val connection = url.openConnection().apply {
```
How can I resolve this? If you propose a fix, please make it concise.
Additional Comments (1)
The content inside the This doesn't cause a compile error, but makes the code harder to read and maintain. Consider reformatting the Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time! Prompt To Fix With AIThis is a comment left during a code review.
Path: examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/chat/ChatScreen.kt
Line: 132-181
Comment:
**Inconsistent indentation inside `Box` scope**
The content inside the `Column` (lines 133-180) is indented at 5 levels (20 spaces), while the sibling elements in the `Box` scope (`AnimatedVisibility`, `ModelRequiredOverlay`, `ModelLoadedToast` at lines 183-209) are indented at 3 levels (12 spaces). The `Column` closing brace at line 181 is also at 3 levels, matching the `Box` scope correctly, but the inner content has excess indentation.
This doesn't cause a compile error, but makes the code harder to read and maintain. Consider reformatting the `Column` content to use consistent 4-level indentation (16 spaces) to match the surrounding code.
<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>
How can I resolve this? If you propose a fix, please make it concise. |
] (#420) * refactor * fixing lora
|
@coderabbitai please do a detailed review. Just added some changes for ios + refactored few things. So, do detailed review. Thanks! |
|
@greptile please do a detailed review. Just added some changes for ios + refactored few things. So, do detailed review. Thanks! |
|
✅ Actions performedReview triggered.
|
| var downloadComplete = false | ||
| try { | ||
| withContext(Dispatchers.IO) { | ||
| val connection = URL(entry.downloadUrl).openConnection().apply { |
There was a problem hiding this comment.
No URL scheme validation — HTTP downloads allowed
URL(entry.downloadUrl).openConnection() doesn't verify HTTPS. If a catalog entry has an http:// URL, the adapter downloads over an unencrypted connection, exposing users to MITM attacks that could inject malicious model files.
| val connection = URL(entry.downloadUrl).openConnection().apply { | |
| if (!entry.downloadUrl.startsWith("https://", ignoreCase = true)) { | |
| throw Exception("Only HTTPS downloads are allowed for security") | |
| } | |
| val connection = URL(entry.downloadUrl).openConnection().apply { |
Prompt To Fix With AI
This is a comment left during a code review.
Path: examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/lora/LoraViewModel.kt
Line: 189
Comment:
No URL scheme validation — HTTP downloads allowed
`URL(entry.downloadUrl).openConnection()` doesn't verify HTTPS. If a catalog entry has an `http://` URL, the adapter downloads over an unencrypted connection, exposing users to MITM attacks that could inject malicious model files.
```suggestion
if (!entry.downloadUrl.startsWith("https://", ignoreCase = true)) {
throw Exception("Only HTTPS downloads are allowed for security")
}
val connection = URL(entry.downloadUrl).openConnection().apply {
```
How can I resolve this? If you propose a fix, please make it concise.| readTimeout = 60_000 | ||
| } | ||
| connection.connect() | ||
| val totalSize = connection.contentLengthLong.takeIf { it > 0 } ?: entry.fileSize |
There was a problem hiding this comment.
Downloaded file size never validated against catalog entry
The code downloads to completion without checking if the downloaded size matches entry.fileSize. A truncated or corrupted download would be silently renamed to the final destination, potentially causing crashes when loading the adapter.
| val totalSize = connection.contentLengthLong.takeIf { it > 0 } ?: entry.fileSize | |
| val totalSize = connection.contentLengthLong.takeIf { it > 0 } ?: entry.fileSize | |
| var downloaded = 0L | |
| connection.getInputStream().buffered().use { input -> | |
| tmpFile.outputStream().buffered().use { output -> | |
| val buffer = ByteArray(8192) | |
| var bytesRead: Int | |
| while (input.read(buffer).also { bytesRead = it } != -1) { | |
| output.write(buffer, 0, bytesRead) | |
| downloaded += bytesRead | |
| if (totalSize > 0) { | |
| val progress = (downloaded.toFloat() / totalSize).coerceIn(0f, 1f) | |
| _uiState.update { it.copy(downloadProgress = progress) } | |
| } | |
| } | |
| } | |
| } | |
| // Validate downloaded file size | |
| if (entry.fileSize > 0 && downloaded != entry.fileSize) { | |
| tmpFile.delete() | |
| throw Exception("Downloaded file size ($downloaded bytes) does not match expected size (${entry.fileSize} bytes)") | |
| } |
Prompt To Fix With AI
This is a comment left during a code review.
Path: examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/lora/LoraViewModel.kt
Line: 194
Comment:
Downloaded file size never validated against catalog entry
The code downloads to completion without checking if the downloaded size matches `entry.fileSize`. A truncated or corrupted download would be silently renamed to the final destination, potentially causing crashes when loading the adapter.
```suggestion
val totalSize = connection.contentLengthLong.takeIf { it > 0 } ?: entry.fileSize
var downloaded = 0L
connection.getInputStream().buffered().use { input ->
tmpFile.outputStream().buffered().use { output ->
val buffer = ByteArray(8192)
var bytesRead: Int
while (input.read(buffer).also { bytesRead = it } != -1) {
output.write(buffer, 0, bytesRead)
downloaded += bytesRead
if (totalSize > 0) {
val progress = (downloaded.toFloat() / totalSize).coerceIn(0f, 1f)
_uiState.update { it.copy(downloadProgress = progress) }
}
}
}
}
// Validate downloaded file size
if (entry.fileSize > 0 && downloaded != entry.fileSize) {
tmpFile.delete()
throw Exception("Downloaded file size ($downloaded bytes) does not match expected size (${entry.fileSize} bytes)")
}
```
How can I resolve this? If you propose a fix, please make it concise.| val uiState by viewModel.uiState.collectAsStateWithLifecycle() | ||
| val listState = rememberLazyListState() | ||
| val scope = rememberCoroutineScope() | ||
| val loraViewModel: LoraViewModel = viewModel() |
There was a problem hiding this comment.
New LoraViewModel instance created on every recomposition
viewModel() without a factory or key creates a fresh instance on recomposition, causing loss of download progress, loaded adapters state, and duplicate registry operations.
| val loraViewModel: LoraViewModel = viewModel() | |
| val loraViewModel: LoraViewModel = viewModel( | |
| factory = object : ViewModelProvider.Factory { | |
| override fun <T : ViewModel> create(modelClass: Class<T>): T { | |
| @Suppress("UNCHECKED_CAST") | |
| return LoraViewModel(LocalContext.current.applicationContext as Application) as T | |
| } | |
| } | |
| ) |
Prompt To Fix With AI
This is a comment left during a code review.
Path: examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/chat/ChatScreen.kt
Line: 80
Comment:
New `LoraViewModel` instance created on every recomposition
`viewModel()` without a factory or key creates a fresh instance on recomposition, causing loss of download progress, loaded adapters state, and duplicate registry operations.
```suggestion
val loraViewModel: LoraViewModel = viewModel(
factory = object : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
@Suppress("UNCHECKED_CAST")
return LoraViewModel(LocalContext.current.applicationContext as Application) as T
}
}
)
```
How can I resolve this? If you propose a fix, please make it concise.| private let logger = SDKLogger(category: "CppBridge.LoraRegistry") | ||
|
|
||
| private init() { | ||
| handle = rac_get_lora_registry() |
There was a problem hiding this comment.
Swift now uses the global singleton registry via rac_get_lora_registry(), matching the Kotlin pattern. This resolves the architectural inconsistency from the previous thread where Swift had a separate registry instance.
Prompt To Fix With AI
This is a comment left during a code review.
Path: sdk/runanywhere-swift/Sources/RunAnywhere/Foundation/Bridge/Extensions/CppBridge+LoraRegistry.swift
Line: 24
Comment:
Swift now uses the global singleton registry via `rac_get_lora_registry()`, matching the Kotlin pattern. This resolves the architectural inconsistency from the previous thread where Swift had a separate registry instance.
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
Actionable comments posted: 9
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (5)
examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/models/ModelSelectionViewModel.kt (1)
161-168: 🛠️ Refactor suggestion | 🟠 MajorAvoid generic Exception handling; use a sealed error type.
CatchingExceptionreduces type safety and makes UI error handling less precise; map failures into a sealed error type and store that in state.As per coding guidelines, use sealed classes for type-safe error handling rather than generic exception handling.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/models/ModelSelectionViewModel.kt` around lines 161 - 168, Replace the generic Exception catch in ModelSelectionViewModel.loadModels (the catch (e: Exception) block) with handling that maps failures to a sealed error type (e.g., define a sealed class ModelLoadError with cases like NetworkError(message), ParsingError(message), UnknownError(cause)). Update the ViewModel to catch specific exceptions (or a broad Throwable only to map to ModelLoadError.UnknownError) and set _uiState.error to a ModelLoadError instance instead of a raw string; adjust the UI state type (e.g., UiState.error) to hold ModelLoadError and update callers to render error messages from the sealed type.examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/tts/TextToSpeechViewModel.kt (1)
395-463:⚠️ Potential issue | 🟠 MajorAvoid logging raw input text to protect user privacy.
Even truncated input can contain PII. Prefer logging metadata (length) instead.
🔒 Proposed fix (redact input)
- Timber.i("Generating speech for text: ${text.take(50)}...") + Timber.i("Generating speech for text: length=${text.length} chars")🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/tts/TextToSpeechViewModel.kt` around lines 395 - 463, The code currently logs raw (truncated) user input via Timber.i("Generating speech for text: ${text.take(50)}..."), which can expose PII; update the logging in the TextToSpeechViewModel generation flow (the block that calls speakSystemTts(...) and RunAnywhere.synthesize(...), and references variables text, options, generatedAudioData) to never include the input text—log only metadata such as text length, selected model id, and options (e.g., Timber.i("Generating speech: length=%d, model=%s", text.length, _uiState.value.selectedModelId)); also scan the same method for any other logs that reference text and remove/redact them. Ensure no exception or info logs concatenate or interpolate the raw text anywhere in this function.examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/voice/VoiceAssistantViewModel.kt (1)
319-323:⚠️ Potential issue | 🟠 MajorAvoid logging raw transcripts/responses in info logs.
These logs include user speech and model responses, which can contain sensitive data. Prefer redacted/length-only logging or restrict to debug builds.🔒 Suggested redaction pattern (apply similarly in other locations)
- Timber.i( - "✅ Voice pipeline result - speechDetected: ${result.speechDetected}, " + - "transcription: ${transcription?.take(50)}, " + - "response: ${response?.take(50)}", - ) + Timber.i( + "✅ Voice pipeline result - speechDetected: ${result.speechDetected}, " + + "transcriptionLength: ${transcription?.length ?: 0}, " + + "responseLength: ${response?.length ?: 0}", + )Also applies to: 895-901, 1006-1010
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/voice/VoiceAssistantViewModel.kt` around lines 319 - 323, The info log in VoiceAssistantViewModel is printing raw user transcripts/responses via Timber.i("... transcription: ${transcription?.take(50)}, response: ${response?.take(50)}"), which leaks sensitive data; change these to redacted or length-only logs and restrict them to non-production (e.g., use BuildConfig.DEBUG) or lower-severity debug logs. Update the logging in the block that references result, transcription, and response (and the similar logging sites noted later) to either log transcription?.length and response?.length or a fixed "[REDACTED]" marker, or wrap the Timber call in a debug-only guard, and replace Timber.i with Timber.d for debug-only output so raw content is never emitted at info level.examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/domain/services/AudioCaptureService.kt (2)
32-34:⚠️ Potential issue | 🟡 MinorRename Android-specific service to include the Android prefix.
This class is Android-only (AudioRecord, MediaRecorder) but its name omits the required
Androidprefix. Consider renaming the class/file toAndroidAudioCaptureServiceand updating call sites.As per coding guidelines, "Name Android-specific implementations with clear 'Android' prefix (e.g., 'AndroidTTSService.kt' not 'TTSService.kt' or 'TTSServiceImpl.kt')".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/domain/services/AudioCaptureService.kt` around lines 32 - 34, The class AudioCaptureService is Android-specific and must be renamed to AndroidAudioCaptureService; update the class declaration (AudioCaptureService -> AndroidAudioCaptureService), rename the containing file accordingly, and update all call sites and imports that reference AudioCaptureService (constructors, type annotations, dependency injection bindings) to the new AndroidAudioCaptureService identifier so references to AudioRecord/MediaRecorder remain consistent.
108-123:⚠️ Potential issue | 🟠 MajorClose the flow on AudioRecord read errors.
When
bytesRead < 0, the read loop breaks but the flow stays open until the collector cancels, potentially leaving capture resources held and the collector waiting indefinitely. Close the channel with an exception to triggerawaitClosecleanup, consistent with the pattern already used on line 95 for initialization errors.🔧 Suggested fix
} else if (bytesRead < 0) { Timber.w("AudioRecord read error: $bytesRead") + close(IllegalStateException("AudioRecord read error: $bytesRead")) break }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/domain/services/AudioCaptureService.kt` around lines 108 - 123, When AudioRecord.read returns bytesRead < 0 the loop currently breaks but does not close the callbackFlow channel; update the read-error branch inside the recording loop in AudioCaptureService (where audioRecord.read, calculateRMS, _audioLevel, trySend are used) to close the flow with an exception (e.g. call close(Throwable(...)) on the channel) instead of merely breaking so that awaitClose runs and resources are released; ensure the exception provides context about the read error code so upstream collectors are notified and cleanup executes.
♻️ Duplicate comments (14)
examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/data/ModelList.kt (1)
84-126: LoRA adapter architecture mismatch and file sizes remain unresolved.The two issues from the prior review still apply to this code:
- These Qwen-trained adapters (
Void2377/Qwen) are listed as compatible with LFM2 models — architecturally different base models. Loading will fail or produce corrupted inference unless the adapters were actually trained against LFM2.- All
fileSizevalues are hardcoded to690_176but the actual HuggingFace file sizes differ (per the previous verification).Please confirm these adapters genuinely work at runtime with the LFM2 base models and update the file sizes to match the actual artifacts.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/data/ModelList.kt` around lines 84 - 126, The loraAdapters entries in ModelList.kt incorrectly claim compatibility with LFM2 and use placeholder fileSize values; verify each LoraAdapterCatalogEntry (e.g., ids "chat-assistant-lora", "summarizer-lora", "translator-lora", "sentiment-lora" inside the loraAdapters list) are actually trained for the LFM2 architecture before declaring them compatibleModelIds, and update each downloadUrl/filename/fileSize to the real HuggingFace artifact metadata (or remove/mark incompatible adapters) so runtime loading won't fail; modify the compatibleModelIds and fileSize fields on those LoraAdapterCatalogEntry instances to reflect confirmed compatibility and exact file sizes.examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/chat/ChatViewModel.kt (1)
803-813: Duplicate: refreshLoraState() can race. Same concern as prior review—multiple concurrent launches can overwrite state.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/chat/ChatViewModel.kt` around lines 803 - 813, refreshLoraState() can race when multiple viewModelScope.launch coroutines run concurrently and overwrite _uiState; guard the read/update with a coroutine-safe lock (e.g., a Mutex like loraStateMutex) or serialize updates via an actor so only one coroutine reads RunAnywhere.getLoadedLoraAdapters() and updates _uiState at a time; wrap the body of refreshLoraState (the call to RunAnywhere.getLoadedLoraAdapters() and the _uiState.value = _uiState.value.copy(...)) in the mutex's withLock (or send a message to the actor) to prevent concurrent overwrites.examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/lora/LoraViewModel.kt (2)
171-240:⚠️ Potential issue | 🟠 Major
cancelDownload()doesn’t actually stop the download loop.The blocking read ignores coroutine cancellation, so a “cancel” can still finish the download and update state. Add cooperative cancellation inside the loop (and avoid surfacing cancellation as an error).
🐛 Suggested fix (cooperative cancellation)
+import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.ensureActive ... try { withContext(Dispatchers.IO) { val connection = URL(entry.downloadUrl).openConnection().apply { connectTimeout = 30_000 readTimeout = 60_000 } connection.connect() val totalSize = connection.contentLengthLong.takeIf { it > 0 } ?: entry.fileSize var downloaded = 0L connection.getInputStream().buffered().use { input -> tmpFile.outputStream().buffered().use { output -> val buffer = ByteArray(8192) var bytesRead: Int while (input.read(buffer).also { bytesRead = it } != -1) { + ensureActive() output.write(buffer, 0, bytesRead) downloaded += bytesRead if (totalSize > 0) { val progress = (downloaded.toFloat() / totalSize).coerceIn(0f, 1f) _uiState.update { it.copy(downloadProgress = progress) } } } } } destFile.delete() if (!tmpFile.renameTo(destFile)) { tmpFile.delete() throw Exception("Failed to move downloaded file to final location") } downloadComplete = true } ... - } catch (e: Exception) { + } catch (e: CancellationException) { + Timber.i("Download cancelled: ${entry.name}") + throw e + } catch (e: Exception) { Timber.e(e, "Failed to download LoRA adapter: ${entry.name}") _uiState.update { it.copy( downloadingAdapterId = null, downloadProgress = 0f, error = "Download failed: ${e.message}", ) } } finally {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/lora/LoraViewModel.kt` around lines 171 - 240, The download loop in downloadAdapter blocks cancellation because InputStream.read is blocking; make it cooperative by checking coroutineContext.isActive (or CoroutineScope.isActive) inside the while loop after each read and break/throw CancellationException if not active, and call withContext(Dispatchers.IO) { ... } remains but add a check like if (!isActive) throw CancellationException() so cancellation stops immediately; also update the catch block to handle CancellationException separately (do not set uiState.error for cancellation, clear downloadingAdapterId and progress) and ensure tmpFile is deleted in finally when cancelled—update references in downloadAdapter and the coroutine cancellation handling so cancelDownload actually stops the download and does not surface cancellation as an error.
154-163: Avoid synchronousFile.exists()in UI-driven calls.If these helpers are invoked during recomposition (e.g., lists in the picker/manager), per-item disk I/O can add UI jank. Prefer caching downloaded IDs in
LoraUiStateduringrefresh()and using that set in composables.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/lora/LoraViewModel.kt` around lines 154 - 163, The localPath(...) and isDownloaded(...) helpers perform synchronous File.exists() disk I/O which can cause UI jank when called during recomposition; instead, populate and maintain a cached set of downloaded filenames/IDs in LoraUiState during refresh() (scan loraDir once in refresh()), expose that cached set to the UI, and change localPath and isDownloaded to query that in-memory set (and return null/false if not present) so composables never call File.exists() directly; ensure refresh() updates the cache when downloads or deletions occur.examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/lora/LoraAdapterPickerSheet.kt (1)
226-226:⚠️ Potential issue | 🟡 MinorReset scale when default changes.
Line 226 only keys
rememberbyentry.id, so the slider won’t reset ifentry.defaultScalechanges for the same id.🛠️ Suggested fix
- var scale by remember(entry.id) { mutableFloatStateOf(entry.defaultScale) } + var scale by remember(entry.id, entry.defaultScale) { mutableFloatStateOf(entry.defaultScale) }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/lora/LoraAdapterPickerSheet.kt` at line 226, The slider state `scale` in LoraAdapterPickerSheet is only remembered by `entry.id` so it won't reset if `entry.defaultScale` changes; update the remember key to include `entry.defaultScale` (i.e., remember by both `entry.id` and `entry.defaultScale`) so the mutableFloatStateOf is reinitialized when the default scale changes, ensuring the slider resets appropriately.examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/MainActivity.kt (1)
78-80:⚠️ Potential issue | 🟡 MinorMove the Ready-state log into a side-effect.
Line 79 logs inside the composable body, so it executes on every recomposition.
🛠️ Suggested fix
@@ -import androidx.compose.runtime.Composable +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect @@ is SDKInitializationState.Ready -> { - Timber.i("App is ready to use!") + LaunchedEffect(Unit) { + Timber.i("App is ready to use!") + } AppNavigation() }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/MainActivity.kt` around lines 78 - 80, The Timber.i("App is ready to use!") call is inside the composable body and will run on every recomposition; move it into a one-time side-effect so it only runs when the state becomes SDKInitializationState.Ready. Specifically, when you handle SDKInitializationState.Ready around AppNavigation(), invoke the log inside a side-effect such as LaunchedEffect(key1 = SDKInitializationState.Ready) or rememberUpdatedState/DisposableEffect tied to the ready state (referencing SDKInitializationState.Ready and AppNavigation) so the log runs only once when the state transitions to Ready.examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/chat/ChatScreen.kt (1)
913-929:⚠️ Potential issue | 🟡 MinorConditionally apply assistant-icon padding in AnalyticsFooter.
Line 928 always reserves icon padding; when
message.modelInfois null, timestamps/analytics shift right unnecessarily. Pass a flag from the caller to control padding.🛠️ Suggested fix
@@ AnalyticsFooter( message = message, hasThinking = message.thinkingContent != null, alignEnd = false, + hasModelIcon = message.modelInfo != null, ) @@ fun AnalyticsFooter( message: ChatMessage, hasThinking: Boolean, - alignEnd: Boolean = true, + alignEnd: Boolean = true, + hasModelIcon: Boolean = true, ) { @@ - modifier = if (alignEnd) Modifier.padding(start = Dimensions.mediumLarge) else Modifier.padding(start = Dimensions.assistantIconSize + Dimensions.assistantIconSpacing), + modifier = + if (alignEnd) { + Modifier.padding(start = Dimensions.mediumLarge) + } else { + Modifier.padding(start = if (hasModelIcon) Dimensions.assistantIconSize + Dimensions.assistantIconSpacing else 0.dp) + }, ) {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/chat/ChatScreen.kt` around lines 913 - 929, AnalyticsFooter currently always reserves assistant-icon padding which shifts timestamps when message.modelInfo is null; add a Boolean parameter (e.g., showAssistantIconPadding: Boolean) to AnalyticsFooter signature and have the inner Row choose its start padding based on that flag (keep existing start = Dimensions.mediumLarge when alignEnd is true, otherwise use Dimensions.assistantIconSize + Dimensions.assistantIconSpacing only when showAssistantIconPadding is true, else no extra start padding). Update callers to pass message.modelInfo != null (or equivalent) so padding is only reserved when the assistant icon will actually render.sdk/runanywhere-kotlin/src/jvmAndroidMain/kotlin/com/runanywhere/sdk/foundation/bridge/extensions/CppBridgeLoraRegistry.kt (2)
27-38: Replace RuntimeException with SDKError (duplicate).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-kotlin/src/jvmAndroidMain/kotlin/com/runanywhere/sdk/foundation/bridge/extensions/CppBridgeLoraRegistry.kt` around lines 27 - 38, The register function currently throws a generic RuntimeException on registration failure; replace that with the SDK-specific error type SDKError: in CppBridgeLoraRegistry.register, after calling RunAnywhereBridge.racLoraRegistryRegister (and detecting result != RunAnywhereBridge.RAC_SUCCESS), construct and throw an SDKError carrying the error code/message instead of RuntimeException (include entry.id in the error context), so error handling uses the SDKError type for downstream handling.
73-117: JSON parsing still ignores braces inside quoted strings (duplicate).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-kotlin/src/jvmAndroidMain/kotlin/com/runanywhere/sdk/foundation/bridge/extensions/CppBridgeLoraRegistry.kt` around lines 73 - 117, The JSON parsing routines (parseLoraEntryArrayJson and extractStringArray) incorrectly count braces/brackets that appear inside quoted strings; update both parsers to track string state and escaped characters so braces/brackets inside quotes are ignored (i.e., maintain an inString boolean toggled by unescaped '"' and skip brace/bracket depth changes when inString is true, and handle backslash escapes correctly), and apply the same string-aware logic to extractString and any regex-based extraction so they don't stop on characters inside quoted strings.sdk/runanywhere-commons/src/jni/runanywhere_commons_jni.cpp (2)
1953-1972:⚠️ Potential issue | 🟠 MajorAvoid JSON string marshaling for LoRA entries in JNI.
loraEntryToJsonand the registry getters return JSON strings, which conflicts with the structured‑types guideline. Prefer returning typed JNI objects/arrays (e.g., aLoraEntryclass or a structured array) instead of JSON.As per coding guidelines, "Always use structured types, never use strings directly for consistency and scalability".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-commons/src/jni/runanywhere_commons_jni.cpp` around lines 1953 - 1972, The current loraEntryToJson function serializes rac_lora_entry_t to a JSON string; replace this with construction of a typed JNI object (e.g., a Java LoraEntry class) and return a jobject (or jobjectArray for multiple entries) instead of a jstring: change loraEntryToJson to take JNIEnv*, find/Cache the LoraEntry jclass and its constructor/field IDs, create a new LoraEntry instance, set string fields with NewStringUTF for id/name/description/download_url/filename, set numeric fields using SetLongField/SetDoubleField (matching file_size/default_scale), build a Java String[] or List for compatible_model_ids from entry->compatible_model_ids and set that field, and update the registry getter functions that currently call loraEntryToJson to return the constructed jobject or jobjectArray so JNI callers receive structured types rather than JSON.
1215-1328:⚠️ Potential issue | 🟠 MajorAdd JNI wrappers for the remaining LoRA registry C APIs.
rac_lora_registry_removeandrac_lora_registry_getare exported but not marshaled through JNI, which breaks the “marshal all C APIs” requirement. Please add JNI functions (and Kotlin externs) for these two calls.As per coding guidelines, "JNI bridge files must use
_jnisuffix and provide Java/Kotlin type marshaling for all C API functions".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-commons/src/jni/runanywhere_commons_jni.cpp` around lines 1215 - 1328, Add JNI wrappers for rac_lora_registry_remove and rac_lora_registry_get by implementing functions Java_com_runanywhere_sdk_native_bridge_RunAnywhereBridge_racLoraRegistryRemove(JNIEnv*, jclass, jstring id) and Java_com_runanywhere_sdk_native_bridge_RunAnywhereBridge_racLoraRegistryGet(JNIEnv*, jclass, jstring id) modeled after racLoraRegistryRegister/racLoraRegistryGetForModel and racLoraRegistryGetAll: call env->GetStringUTFChars for the jstring, call rac_lora_registry_remove(registry, id_str) (or rac_lora_registry_get(registry, id_str, &entry) respectively), ReleaseStringUTFChars, convert returned rac_lora_entry_t* to JSON using loraEntryToJson for the Get wrapper (and free the returned entry via rac_lora_entry_free or the appropriate free function), return jint result for Remove and jstring JSON (or "null"/"[]" as appropriate) for Get, and handle null checks, memory allocation/frees and error logging consistent with existing JNI functions; also add corresponding Kotlin extern declarations for these two JNI functions.sdk/runanywhere-commons/include/rac/core/rac_core.h (1)
342-367:⚠️ Potential issue | 🟠 MajorExpand LoRA API docs with explicit error codes and ownership semantics.
The docblocks still read as generic “RAC_SUCCESS or error code” and don’t spell out which RAC_ERROR_* values apply or that the registry handle is library‑owned; please enumerate the concrete error codes and ownership/lifecycle expectations.📄 Doc tweak example
- * `@return` Handle to the global registry (never NULL after first successful call) + * `@return` Handle to the global registry, or NULL if creation failed. + * `@note` The registry handle is owned by the library; do not free it. ... - * `@return` RAC_SUCCESS or error code + * `@return` RAC_SUCCESS on success, or a specific RAC_ERROR_* on failure. + * `@note` out_entries/out_count must be non-NULL. The returned array and entries + * are owned by the caller and must be freed with rac_lora_entry_array_free.As per coding guidelines, "Public C API headers in include/rac/ must document vtable operations, error codes, and lifecycle requirements".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-commons/include/rac/core/rac_core.h` around lines 342 - 367, Update the three LoRA API docblocks to enumerate applicable error codes and explicit ownership/lifecycle rules: for rac_get_lora_registry() document that it returns a process‑lifetime, library‑owned pointer to struct rac_lora_registry (caller must not free) and list possible returns such as RAC_SUCCESS and RAC_ERROR_NO_MEMORY/RAC_ERROR_INTERNAL; for rac_register_lora(const struct rac_lora_entry* entry) state that the entry is deep‑copied by the library, the caller retains ownership of the supplied rac_lora_entry, and enumerate errors like RAC_SUCCESS, RAC_ERROR_INVALID_ARG, RAC_ERROR_ALREADY_EXISTS, RAC_ERROR_NO_MEMORY and RAC_ERROR_INTERNAL; for rac_get_lora_for_model(const char* model_id, struct rac_lora_entry*** out_entries, size_t* out_count) document ownership of the returned out_entries (caller must free with rac_lora_entry_array_free), that out_count is set on success, and list errors such as RAC_SUCCESS, RAC_ERROR_INVALID_ARG, RAC_ERROR_NOT_FOUND, RAC_ERROR_NO_MEMORY and RAC_ERROR_INTERNAL; also mention any vtable/operation behavior on rac_lora_entry if relevant.sdk/runanywhere-commons/src/infrastructure/model_management/lora_registry.cpp (1)
24-45:deep_copy_lora_entrysilently returns partially-initialized entry on inner allocation failure.This was previously flagged: when
mallocforcompatible_model_ids(Line 34) fails, the function returns a non-nullcopywithcompatible_model_ids = nullptrandcompatible_model_count = 0, silently dropping the compatibility list. Similarly,rac_strdupfailures on Lines 28–32 are not checked. All callers only checkif (!copy)and proceed with the partial data.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-commons/src/infrastructure/model_management/lora_registry.cpp` around lines 24 - 45, deep_copy_lora_entry currently can return a partially-initialized rac_lora_entry_t when any rac_strdup or the compatible_model_ids malloc (or any strdup inside the loop) fails; update deep_copy_lora_entry to check the result of each rac_strdup for id/name/description/download_url/filename and the malloc for compatible_model_ids and each strdup in the compatible_model_ids loop, and if any allocation fails free any previously allocated strings, the compatible_model_ids array, and the copy struct itself, then return nullptr; ensure copy->compatible_model_count is only set after the entire compatible_model_ids block succeeds so callers never receive partial data.sdk/runanywhere-commons/include/rac/infrastructure/model_management/rac_lora_registry.h (1)
44-115: Several@returndocs still say generic "or error code" rather than listing specific codes.For
rac_lora_registry_create,rac_lora_registry_register,rac_lora_registry_get_all, andrac_lora_registry_get_for_model, the implementation can returnRAC_ERROR_INVALID_ARGUMENTandRAC_ERROR_OUT_OF_MEMORY, but the docs only say "or error code". Theremoveandgetfunctions correctly enumerate their specific codes (RAC_ERROR_NOT_FOUND). As per coding guidelines: "Public C API headers in include/rac/ must document error codes."🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-commons/include/rac/infrastructure/model_management/rac_lora_registry.h` around lines 44 - 115, The `@return` docs for rac_lora_registry_create, rac_lora_registry_register, rac_lora_registry_get_all, and rac_lora_registry_get_for_model are too generic; update their comment lines to enumerate the specific error codes the implementations can return (at minimum list RAC_SUCCESS, RAC_ERROR_INVALID_ARGUMENT, and RAC_ERROR_OUT_OF_MEMORY) so they match the implementation and the header guideline; leave rac_lora_registry_remove and rac_lora_registry_get as-is since they already list RAC_ERROR_NOT_FOUND.
🧹 Nitpick comments (11)
examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/chat/components/MarkdownText.kt (2)
415-423: Hardcoded link color won't adapt to theme changes.
Color(0xFF3B82F6)is a fixed blue that doesn't respond to dark/light theme. SinceparseInlineMarkdownisn't a@Composable, it can't accessMaterialTheme— but you could pass alinkColorparameter alongsidedefaultColor.♻️ Suggested approach
-private fun parseInlineMarkdown(text: String, defaultColor: Color): AnnotatedString { +private fun parseInlineMarkdown(text: String, defaultColor: Color, linkColor: Color = Color(0xFF3B82F6)): AnnotatedString {Then at the call sites (within
@Composablescope), pass a theme-derived color:val annotated = parseInlineMarkdown( block.text, color, linkColor = MaterialTheme.colorScheme.primary, )And use it in the link style:
style = SpanStyle( - color = Color(0xFF3B82F6), + color = linkColor, textDecoration = TextDecoration.Underline, ),🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/chat/components/MarkdownText.kt` around lines 415 - 423, The code hardcodes link color using Color(0xFF3B82F6) inside the LinkAnnotation.Url/TextLinkStyles/SpanStyle creation, which won't follow light/dark themes; update parseInlineMarkdown to accept a new linkColor parameter (in addition to the existing default color parameter), replace the hardcoded Color(0xFF3B82F6) with that linkColor when building TextLinkStyles/SpanStyle, and update call sites (from within `@Composable` scopes) to pass MaterialTheme.colorScheme.primary (or another theme-derived color) as the linkColor so links adapt to theme changes.
346-405: Inline formatting doesn't support nesting (e.g., bold text containing a link).The bold/italic/code handlers use
append(text.substring(...))directly without recursively parsing inner content. This means**bold [link](url)**renders the brackets and parentheses literally. This is fine for a lightweight renderer, but worth documenting in the KDoc so future maintainers know this is a deliberate simplification.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/chat/components/MarkdownText.kt` around lines 346 - 405, The inline formatting handlers in MarkdownText.kt (the bold/italic/***/ and inline code branches that call append(text.substring(...)) ) do not recursively parse nested inline elements (so constructs like **bold [link](url)** will render brackets/parentheses literally); add a KDoc comment to the MarkdownText.kt function containing these handlers (or at the top of the file near the parser/buildAnnotatedString function) stating that inline formatting intentionally does not support nesting and describing this limitation (mentioning the specific branches: the triple-asterisk handler, the double-asterisk handler, the single-asterisk handler, and the backtick handler) so future maintainers understand this is a deliberate simplification.examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/voice/VoiceAssistantParticleView.kt (2)
391-405: Remove the redundant transparent drawPoints call.
drawBatchedByColoralready draws the batched points; the precedingdrawPointswithColor.Transparentadds a no-op draw call in a hot path.♻️ Suggested simplification
- // drawPoints with StrokeCap.Round renders small circles efficiently - drawPoints( - points = batchPoints, - pointMode = PointMode.Points, - color = Color.Transparent, // overridden per-point below - strokeWidth = 4f, - cap = StrokeCap.Round, - ) - // Unfortunately drawPoints doesn't support per-point color, - // so we draw in color buckets for efficiency. - // Group by quantized color to reduce draw calls. + // drawPoints doesn't support per-point color, + // so we draw in color buckets for efficiency. + // Group by quantized color to reduce draw calls. drawBatchedByColor(batchPoints, batchColors)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/voice/VoiceAssistantParticleView.kt` around lines 391 - 405, Remove the redundant drawPoints call that uses Color.Transparent inside the batch draw block: the existing drawBatchedByColor(batchPoints, batchColors) already renders the points with per-point colors, so delete the drawPoints(...) invocation (the block referencing drawPoints with strokeWidth=4f and Color.Transparent) to avoid the unnecessary no-op draw in the hot path and leave only drawBatchedByColor(batchPoints, batchColors).
412-429: Avoid per-frame bucket allocations in the hot path.
drawBatchedByColorbuilds a newHashMapand per-bucketArrayLists every frame. That GC churn can offset the intended batching gains. Consider reusing bucket structures across frames (e.g., hoist/reuse a mutable map + lists and clear them each frame).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/voice/VoiceAssistantParticleView.kt` around lines 412 - 429, drawBatchedByColor currently allocates a fresh HashMap and new ArrayLists each frame (buckets + per-bucket lists via getOrPut), causing GC churn; refactor by hoisting the buckets map and a small object pool of MutableList<Offset> to the surrounding class (reuseBuckets: MutableMap<Long, MutableList<Offset>> and a reusableListPool), then in drawBatchedByColor clear reuseBuckets and any lists pulled from the map (or return cleared lists to the pool) instead of newing ArrayList(points.size/8), and use getOrPut to fetch-and-clear an existing list rather than allocating; ensure lists are cleared before reuse so bucket accumulation logic (key computation using rq/gq/bq/aq) remains unchanged.examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/models/ModelSelectionViewModel.kt (1)
413-413: Remove the empty companion object if it no longer serves a purpose.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/models/ModelSelectionViewModel.kt` at line 413, The file contains an empty companion object in ModelSelectionViewModel (a bare "companion object" declaration) that should be removed; delete the empty companion object block and ensure there are no references to ModelSelectionViewModel.Companion elsewhere (update any call sites or imports if they reference the companion before removing).examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/domain/services/AudioCaptureService.kt (1)
134-157: Preserve stack traces in Timber error logs.The current logging drops the throwable stack trace. Prefer Timber’s throwable overloads for better diagnostics.
🔧 Suggested fix
- } catch (e: Exception) { - Timber.e("Error in audio capture: ${e.message}") + } catch (e: Exception) { + Timber.e(e, "Error in audio capture") stopCaptureInternal() close(e) } @@ - } catch (e: Exception) { - Timber.w("Error stopping audio capture: ${e.message}") + } catch (e: Exception) { + Timber.w(e, "Error stopping audio capture") }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/domain/services/AudioCaptureService.kt` around lines 134 - 157, The current catch blocks drop stack traces by logging only e.message; update the Timber calls to use the throwable overloads so the stack trace is preserved: in the audio capture catch replace Timber.e("Error in audio capture: ${e.message}") with Timber.e(e, "Error in audio capture") and in stopCaptureInternal's catch replace Timber.w("Error stopping audio capture: ${e.message}") with Timber.w(e, "Error stopping audio capture"); ensure you import/use the same exception variable names (e) and keep the surrounding calls to stopCaptureInternal() and close(e) unchanged.examples/ios/RunAnywhereAI/RunAnywhereAI/Features/Chat/ViewModels/LLMViewModel.swift (1)
533-533: UnstructuredTasknested insideMainActor.run— prefer a stored, cancellable task.
Task { await self.refreshAvailableAdapters() }is unstructured: it cannot be cancelled if the ViewModel is torn down and it strongly retainsself. SinceLLMViewModelis@MainActor, theawait MainActor.run { }wrapper is also redundant here — the entire outerTask {}(line 519) already inherits the main-actor context via@MainActoron the class.♻️ Simplified approach — remove the `MainActor.run` wrapper and kick off the refresh outside it
- await MainActor.run { - self.isModelLoaded = true - // ... other state mutations ... - Task { await self.refreshAvailableAdapters() } - } + self.isModelLoaded = true + // ... other state mutations ... + await self.refreshAvailableAdapters()Since the enclosing
Task {}is already executed on the@MainActorclass,MainActor.runis a no-op here and therefreshAvailableAdapters()call can be awaited directly, making the control-flow explicit and the task cancellable.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/ios/RunAnywhereAI/RunAnywhereAI/Features/Chat/ViewModels/LLMViewModel.swift` at line 533, The nested unstructured Task inside MainActor.run should be removed and the refresh made cancellable: delete the inner Task { await self.refreshAvailableAdapters() } call and instead call await self.refreshAvailableAdapters() directly from the outer Task (since LLMViewModel is `@MainActor`), or—better—create a stored Task?refreshTask: Task<Void, Never>? on LLMViewModel, assign refreshTask = Task { await self.refreshAvailableAdapters() } so you can cancel it in deinit or when the view is torn down (call refreshTask?.cancel()), ensuring refreshAvailableAdapters() is started in a cancellable, non-leaking way and removing the redundant MainActor.run wrapper.examples/ios/RunAnywhereAI/RunAnywhereAI/Features/Chat/Models/DemoLoRAAdapter.swift (1)
22-22: PreferLoggerover bareEvery other file in this module (
LLMViewModel.swift, etc.) usesos.log.Logger. Using♻️ Proposed change
+ private static let logger = Logger(subsystem: "com.runanywhere.RunAnywhereAI", category: "LoRAAdapterCatalog") ... - print("[LoRA] Failed to register adapter \(entry.id): \(error)") + logger.error("[LoRA] Failed to register adapter \(entry.id): \(error.localizedDescription)")🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/ios/RunAnywhereAI/RunAnywhereAI/Features/Chat/Models/DemoLoRAAdapter.swift` at line 22, Replace the bare print call in DemoLoRAAdapter.swift that logs registration failures with os.log.Logger: add an imported Logger (e.g., a static let logger on DemoLoRAAdapter or top-level) and call logger.error/log(level) including the adapter id and the error object (use privacy modifiers like .public/.private as appropriate) instead of print so failures surface to Console.app and system logs; ensure Logger is initialized with a fitting subsystem/category consistent with other files (e.g., same subsystem/category used in LLMViewModel).examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/lora/LoraViewModel.kt (2)
30-37: Prefer a sealed error type over a String inLoraUiState.String errors + generic
Exceptionhandling lose type safety and make UI mapping brittle. Consider a sealed error model (and store the throwable or code) instead of raw strings.♻️ Suggested direction
+sealed interface LoraError { + data class Message(val message: String) : LoraError + data class Cause(val throwable: Throwable) : LoraError +} data class LoraUiState( val registeredAdapters: List<LoraAdapterCatalogEntry> = emptyList(), val loadedAdapters: List<LoRAAdapterInfo> = emptyList(), val compatibleAdapters: List<LoraAdapterCatalogEntry> = emptyList(), val downloadingAdapterId: String? = null, val downloadProgress: Float = 0f, - val error: String? = null, + val error: LoraError? = null, )As per coding guidelines: "Use sealed classes for type-safe error handling rather than generic exception handling" and "Always use structured types and data classes instead of strings directly for type safety and consistency".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/lora/LoraViewModel.kt` around lines 30 - 37, Replace the raw String error in LoraUiState with a sealed error type to gain type safety: define a sealed class (e.g., LoraError with cases like None, Message(val text:String), ThrowableError(val throwable:Throwable), NetworkError(val code:Int) as appropriate) and change the LoraUiState.error property from String? to that sealed type; update any usage sites in the ViewModel (references to LoraUiState and places that set or read error) to construct and consume the new LoraError cases instead of raw strings so UI mapping can pattern-match on well-defined error variants.
39-56: Consider moving LoRA business logic tocommonMain.This ViewModel holds core adapter lifecycle and download logic; the guideline expects that to live in shared code with Android-specific wiring kept thin.
As per coding guidelines: "Define all business logic, protocols, interfaces, and structures in
commonMain/rather than in platform-specific modules".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/lora/LoraViewModel.kt` around lines 39 - 56, The LoraViewModel currently contains core LoRA adapter lifecycle and download logic (see LoraViewModel, refresh(), downloadJob, loraDir, uiState/LoraUiState); move that business logic into a shared commonMain module by extracting a platform-agnostic LoraRepository (or LoraManager) and data models (LoraUiState) that expose suspend functions and flows for list/download/load/remove; keep LoraViewModel in the Android module as a thin adapter that only delegates to the shared LoraRepository and supplies Android-specific File/Context wiring (e.g., create the platform-specific loraDir provider and pass it into the shared repository), update refresh() to call the repository APIs and replace direct file/IO and coroutine logic with injected shared implementations.sdk/runanywhere-kotlin/src/jvmAndroidMain/kotlin/com/runanywhere/sdk/foundation/bridge/extensions/CppBridgeLoraRegistry.kt (1)
13-14: Use a JVM-specific name prefix.
Consider renaming to aJvm*prefix (and file name) to reflect its jvmAndroidMain scope.As per coding guidelines: "
**/jvm*/**/*.kt: Name JVM-specific implementations with clear 'Jvm' prefix".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-kotlin/src/jvmAndroidMain/kotlin/com/runanywhere/sdk/foundation/bridge/extensions/CppBridgeLoraRegistry.kt` around lines 13 - 14, The object CppBridgeLoraRegistry is JVM-specific but uses a generic name; rename the object to JvmCppBridgeLoraRegistry (and update the Kotlin file name accordingly) to follow the "Jvm" prefix convention for jvm*/ sources, then update all references/usages of CppBridgeLoraRegistry across the module (including any imports and registrations) so they point to JvmCppBridgeLoraRegistry; keep the TAG constant value or update it to match the new type name if you prefer consistent logging identifiers.
ℹ️ Review info
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (65)
.idea/vcs.xmlPackage.swiftexamples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/MainActivity.ktexamples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/RunAnywhereApplication.ktexamples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/data/ConversationStore.ktexamples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/data/ModelList.ktexamples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/domain/services/AudioCaptureService.ktexamples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/benchmarks/models/BenchmarkTypes.ktexamples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/benchmarks/views/BenchmarkDashboardScreen.ktexamples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/benchmarks/views/BenchmarkDetailScreen.ktexamples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/chat/ChatScreen.ktexamples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/chat/ChatViewModel.ktexamples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/chat/components/MarkdownText.ktexamples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/chat/components/MessageInput.ktexamples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/chat/components/ModelRequiredOverlay.ktexamples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/components/TopBarState.ktexamples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/lora/LoraAdapterPickerSheet.ktexamples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/lora/LoraManagerScreen.ktexamples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/lora/LoraViewModel.ktexamples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/models/ModelSelectionBottomSheet.ktexamples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/models/ModelSelectionViewModel.ktexamples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/navigation/AppNavigation.ktexamples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/navigation/MoreHubScreen.ktexamples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/rag/DocumentRAGScreen.ktexamples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/rag/RAGViewModel.ktexamples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/settings/SettingsScreen.ktexamples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/settings/SettingsViewModel.ktexamples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/settings/ToolSettingsViewModel.ktexamples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/stt/SpeechToTextScreen.ktexamples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/stt/SpeechToTextViewModel.ktexamples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/tts/TextToSpeechScreen.ktexamples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/tts/TextToSpeechViewModel.ktexamples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/vision/VLMScreen.ktexamples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/vision/VLMViewModel.ktexamples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/vision/VisionHubScreen.ktexamples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/voice/VoiceAssistantParticleView.ktexamples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/voice/VoiceAssistantScreen.ktexamples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/voice/VoiceAssistantViewModel.ktexamples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/theme/AppColors.ktexamples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/theme/Dimensions.ktexamples/ios/RunAnywhereAI/Package.resolvedexamples/ios/RunAnywhereAI/RunAnywhereAI.xcodeproj/project.pbxprojexamples/ios/RunAnywhereAI/RunAnywhereAI/App/RunAnywhereAIApp.swiftexamples/ios/RunAnywhereAI/RunAnywhereAI/Features/Chat/Models/DemoLoRAAdapter.swiftexamples/ios/RunAnywhereAI/RunAnywhereAI/Features/Chat/ViewModels/LLMViewModel.swiftexamples/ios/RunAnywhereAI/RunAnywhereAI/Features/Chat/Views/ChatInterfaceView.swiftsdk/runanywhere-commons/exports/RACommons.exportssdk/runanywhere-commons/include/rac/core/rac_core.hsdk/runanywhere-commons/include/rac/features/llm/rac_llm_component.hsdk/runanywhere-commons/include/rac/infrastructure/model_management/rac_lora_registry.hsdk/runanywhere-commons/scripts/build-ios.shsdk/runanywhere-commons/src/features/llm/llm_component.cppsdk/runanywhere-commons/src/infrastructure/model_management/lora_registry.cppsdk/runanywhere-commons/src/jni/runanywhere_commons_jni.cppsdk/runanywhere-kotlin/src/jvmAndroidMain/kotlin/com/runanywhere/sdk/foundation/bridge/extensions/CppBridgeLoraRegistry.ktsdk/runanywhere-kotlin/src/jvmAndroidMain/kotlin/com/runanywhere/sdk/public/extensions/RunAnywhere+LoRA.jvmAndroid.ktsdk/runanywhere-swift/Sources/RunAnywhere/CRACommons/include/CRACommons.hsdk/runanywhere-swift/Sources/RunAnywhere/CRACommons/include/rac_core.hsdk/runanywhere-swift/Sources/RunAnywhere/CRACommons/include/rac_llm_component.hsdk/runanywhere-swift/Sources/RunAnywhere/CRACommons/include/rac_lora_registry.hsdk/runanywhere-swift/Sources/RunAnywhere/CRACommons/include/rac_model_types.hsdk/runanywhere-swift/Sources/RunAnywhere/Foundation/Bridge/Extensions/CppBridge+LLM.swiftsdk/runanywhere-swift/Sources/RunAnywhere/Foundation/Bridge/Extensions/CppBridge+LoraRegistry.swiftsdk/runanywhere-swift/Sources/RunAnywhere/Public/Extensions/LLM/LLMTypes.swiftsdk/runanywhere-swift/Sources/RunAnywhere/Public/Extensions/LLM/RunAnywhere+LoRA.swift
✅ Files skipped from review due to trivial changes (1)
- examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/data/ConversationStore.kt
🚧 Files skipped from review as they are similar to previous changes (4)
- sdk/runanywhere-commons/include/rac/features/llm/rac_llm_component.h
- sdk/runanywhere-commons/src/features/llm/llm_component.cpp
- sdk/runanywhere-kotlin/src/jvmAndroidMain/kotlin/com/runanywhere/sdk/public/extensions/RunAnywhere+LoRA.jvmAndroid.kt
- examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/models/ModelSelectionBottomSheet.kt
| if (model.files.isNotEmpty()) { | ||
| RunAnywhere.registerMultiFileModel( | ||
| id = model.id, name = model.name, files = model.files, | ||
| framework = model.framework, modality = model.category, | ||
| memoryRequirement = model.memoryRequirement, | ||
| ) | ||
| } else if (model.companionFiles.isNotEmpty()) { | ||
| RunAnywhere.registerMultiFileModel( | ||
| id = model.id, name = model.name, primaryUrl = model.url, | ||
| companionFiles = model.companionFiles, | ||
| framework = model.framework, modality = model.category, | ||
| memoryRequirement = model.memoryRequirement, | ||
| ) | ||
| } else { | ||
| RunAnywhere.registerModel( | ||
| id = model.id, name = model.name, url = model.url, | ||
| framework = model.framework, modality = model.category, | ||
| memoryRequirement = model.memoryRequirement, | ||
| supportsLora = model.supportsLoraAdapters, | ||
| ) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
ast-grep --pattern 'fun registerMultiFileModel($$$)'Repository: RunanywhereAI/runanywhere-sdks
Length of output: 56
🏁 Script executed:
ast-grep --pattern 'fun registerModel($$$)'Repository: RunanywhereAI/runanywhere-sdks
Length of output: 255
🏁 Script executed:
rg "registerMultiFileModel" --type kt -A 5Repository: RunanywhereAI/runanywhere-sdks
Length of output: 100
🏁 Script executed:
rg "registerModel" --type kt -B 2 -A 10 | head -100Repository: RunanywhereAI/runanywhere-sdks
Length of output: 100
🏁 Script executed:
rg "registerMultiFileModel" -A 5 | head -50Repository: RunanywhereAI/runanywhere-sdks
Length of output: 6711
🏁 Script executed:
rg "fun registerModel" -B 2 -A 15 | head -100Repository: RunanywhereAI/runanywhere-sdks
Length of output: 8824
🏁 Script executed:
find . -name "*.kt" -type f | xargs grep -l "registerMultiFileModel" | head -5Repository: RunanywhereAI/runanywhere-sdks
Length of output: 379
🏁 Script executed:
cat -n sdk/runanywhere-kotlin/src/commonMain/kotlin/com/runanywhere/sdk/public/extensions/RunAnywhere+ModelManagement.kt | head -200Repository: RunanywhereAI/runanywhere-sdks
Length of output: 9156
🏁 Script executed:
cat -n sdk/runanywhere-kotlin/src/commonMain/kotlin/com/runanywhere/sdk/public/extensions/RunAnywhere+ModelManagement.kt | sed -n '194,250p'Repository: RunanywhereAI/runanywhere-sdks
Length of output: 2558
🏁 Script executed:
rg "data class ModelInfo" -A 30Repository: RunanywhereAI/runanywhere-sdks
Length of output: 12194
supportsLora flag cannot be set for multi-file models.
The registerMultiFileModel function has no supportsLora parameter, so any multi-file model will always have supportsLora = false regardless of the model's actual capabilities. The single-URL registerModel path accepts and forwards the supportsLora parameter (as shown in the code at line 187), but both multi-file registration paths cannot propagate this flag.
Currently no multi-file model sets this flag, but this is a latent bug that will cause LoRA functionality to be silently unavailable when a future model with LoRA support is registered as multi-file.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/data/ModelList.kt`
around lines 169 - 188, The multi-file registration paths are not propagating
the model.supportsLora flag because registerMultiFileModel currently lacks that
parameter; update both calls to RunAnywhere.registerMultiFileModel (the branch
that uses model.files and the branch that uses primaryUrl + companionFiles) to
include supportsLora = model.supportsLora, and ensure the registerMultiFileModel
API/signature (and its implementation) is extended to accept and forward a
supportsLora boolean so multi-file models can enable LoRA support.
| fun DocumentRAGScreen( | ||
| onBack: () -> Unit = {}, | ||
| viewModel: RAGViewModel = viewModel(), | ||
| ) { |
There was a problem hiding this comment.
Silent no-op back button when onBack is omitted
showBack = true is hardcoded at line 86, while onBack defaults to {}. Because ConfigureTopBar accepts onBack: (() -> Unit)?, a non-null empty lambda is always received—so the back button is always rendered and tappable, but navigates nowhere when the caller omits onBack. This is a subtle UX defect for standalone/preview uses.
Two options:
🛠️ Option A – make `onBack` conditional on `showBack`
fun DocumentRAGScreen(
- onBack: () -> Unit = {},
+ onBack: (() -> Unit)? = null,
viewModel: RAGViewModel = viewModel(),
) {
...
- ConfigureTopBar(title = "Document Q&A", showBack = true, onBack = onBack)
+ ConfigureTopBar(title = "Document Q&A", showBack = onBack != null, onBack = onBack)🛠️ Option B – keep `() -> Unit` but make `showBack` conditional
- ConfigureTopBar(title = "Document Q&A", showBack = true, onBack = onBack)
+ ConfigureTopBar(title = "Document Q&A", showBack = true, onBack = onBack.takeIf { it != {} })(Note: lambda identity comparison is unreliable; Option A is preferred.)
Also applies to: 86-86
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/rag/DocumentRAGScreen.kt`
around lines 48 - 51, The back button is always shown but no-ops because
DocumentRAGScreen currently defaults onBack to an empty lambda; change the API
so onBack is nullable and only provided when the caller intends navigation:
update the DocumentRAGScreen signature to onBack: (() -> Unit)? = null, pass
that nullable onBack through to ConfigureTopBar (which already accepts (() ->
Unit)?), and remove any hardcoded showBack true logic so ConfigureTopBar decides
visibility from the nullable onBack; update any callers/tests/previews to pass a
real lambda where navigation should occur.
| 58RAGBKND02ED16DA40058D03 /* RunAnywhereRAG */ = { | ||
| isa = XCSwiftPackageProductDependency; | ||
| package = 58E021172E52A86000B722EF /* XCLocalSwiftPackageReference "../../.." */; | ||
| productName = RunAnywhereRAG; | ||
| }; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Find Package.swift files and search for RunAnywhereRAG declaration
echo "=== Searching for Package.swift files ==="
fd -a 'Package.swift'
echo -e "\n=== Checking for RunAnywhereRAG in Package.swift files ==="
fd -a 'Package.swift' -x rg -n 'RunAnywhereRAG' {}
echo -e "\n=== Checking full Package.swift content (root level) ==="
if [ -f "Package.swift" ]; then
echo "Root Package.swift found, showing products section:"
rg -A 20 -B 2 'products' Package.swift
fiRepository: RunanywhereAI/runanywhere-sdks
Length of output: 1302
🏁 Script executed:
#!/bin/bash
# Get context around the RunAnywhereRAG declaration
echo "=== Full context around RunAnywhereRAG in Package.swift ==="
rg -B 5 -A 10 'RunAnywhereRAG' Package.swift
echo -e "\n=== Checking for RAG-related product definition ==="
rg -B 3 -A 5 'name.*RAG|RAG.*library' Package.swiftRepository: RunanywhereAI/runanywhere-sdks
Length of output: 2569
Address the conditional product gate for RunAnywhereRAG.
The RunAnywhereRAG product is declared in Package.swift but is conditionally included via the ragProducts() function—it's only available when useLocalBinaries || ragRemoteBinaryAvailable is true. The Xcode project unconditionally requires this product; if either condition is false at build time, the dependency will fail to resolve despite the product existing in the package definition.
Ensure the build configuration guarantees at least one of these conditions is met, or make the xcodeproj dependency conditional to match the package's conditional product definition.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@examples/ios/RunAnywhereAI/RunAnywhereAI.xcodeproj/project.pbxproj` around
lines 712 - 716, The Xcode project unconditionally references the
XCSwiftPackageProductDependency for product RunAnywhereRAG while that product is
conditionally emitted by ragProducts() (depending on useLocalBinaries ||
ragRemoteBinaryAvailable); update either the build configuration so one of
useLocalBinaries or ragRemoteBinaryAvailable is always true at build time, or
change the project.pbxproj entry so the RunAnywhereRAG dependency is added only
when the same condition holds (i.e. make the XCSwiftPackageProductDependency for
RunAnywhereRAG conditional to match ragProducts()), ensuring the project and
Package.swift remain in sync.
| id: "chat-assistant-lora", | ||
| name: "Chat Assistant", | ||
| description: "Enhances conversational chat ability", | ||
| downloadURL: URL(string: "https://huggingface.co/Void2377/Qwen/resolve/main/lora/chat_assistant-lora-Q8_0.gguf")!, |
There was a problem hiding this comment.
Force-unwrapped URL(string:)! literals will crash on malformed input.
All four entries use ! to force-unwrap URL(string:...). While these are currently valid hardcoded strings, the TODO on line 8 explicitly flags upcoming portal-driven catalog replacement, at which point dynamically supplied URL strings could be malformed. Prefer a compile-time assertion or guard let with a meaningful fallback now so the pattern is safe to follow forward.
🛡️ Suggested pattern for a single entry (apply to all 4)
- downloadURL: URL(string: "https://huggingface.co/Void2377/Qwen/resolve/main/lora/chat_assistant-lora-Q8_0.gguf")!,
+ downloadURL: URL(string: "https://huggingface.co/Void2377/Qwen/resolve/main/lora/chat_assistant-lora-Q8_0.gguf")
+ ?? { preconditionFailure("Invalid hardcoded URL for chat-assistant-lora") }(),Or factor URL construction through a helper that validates at init-time once, rather than scattering ! across the catalog.
Also applies to: 43-43, 53-53, 63-63
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@examples/ios/RunAnywhereAI/RunAnywhereAI/Features/Chat/Models/DemoLoRAAdapter.swift`
at line 33, Replace the force-unwrapped URL(string: "...")! literals in
DemoLoRAAdapter (the four catalog entries that currently use URL(string:
"...")!) with safe validation: either use guard let to unwrap and handle the nil
case with a meaningful fallback/error or use a compile-time/runtime assertion
(preconditionFailure or fatalError with a clear message) during initialization
so malformed strings cannot crash later; apply this change to all four entries
so URL construction is centralized and fails fast with an informative message
instead of using force unwraps.
| let loraDir = Self.loraDownloadDirectory() | ||
| try FileManager.default.createDirectory(at: loraDir, withIntermediateDirectories: true) | ||
| let destinationURL = loraDir.appendingPathComponent(adapter.fileName) | ||
| let destinationURL = loraDir.appendingPathComponent(adapter.filename) |
There was a problem hiding this comment.
adapter.filename used unsanitized in path construction — forward-looking path traversal risk.
appendingPathComponent(adapter.filename) is safe today because the catalog is hardcoded, but the TODO in DemoLoRAAdapter.swift explicitly calls out replacing this with portal-provided entries. Once filename can be server-supplied (e.g., "../../Library/Preferences/com.apple.SomeApp.plist"), the current code writes outside Documents/LoRA. Add sanitization before the portal integration lands.
🛡️ Suggested sanitization helper (add to `downloadAdapter` and reuse in `syncDownloadedAdapterPaths`)
private func downloadAdapter(_ adapter: LoraAdapterCatalogEntry) async throws -> String {
let loraDir = Self.loraDownloadDirectory()
try FileManager.default.createDirectory(at: loraDir, withIntermediateDirectories: true)
- let destinationURL = loraDir.appendingPathComponent(adapter.filename)
+ let sanitized = URL(fileURLWithPath: adapter.filename).lastPathComponent // strips any directory components
+ guard !sanitized.isEmpty else { throw LLMError.noModelLoaded /* or a dedicated error */ }
+ let destinationURL = loraDir.appendingPathComponent(sanitized)Apply the same guard in syncDownloadedAdapterPaths (line 442).
Also applies to: 442-442
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@examples/ios/RunAnywhereAI/RunAnywhereAI/Features/Chat/ViewModels/LLMViewModel.swift`
at line 415, The code constructs destinationURL by appending adapter.filename
directly to loraDir which allows path traversal if filename becomes untrusted;
in LLMViewModel methods downloadAdapter and syncDownloadedAdapterPaths, sanitize
adapter.filename first (e.g., use filename = URL(fileURLWithPath:
adapter.filename).lastPathComponent or strip path separators and reject ".." and
empty values, optionally validate against an allowlist/regex) and then call
loraDir.appendingPathComponent(sanitizedFilename) to build destinationURL;
include a fallback/error handling path if sanitization fails and add a brief
comment referencing DemoLoRAAdapter.swift to mark why this check is required.
| rac_result_t rac_lora_registry_create(rac_lora_registry_handle_t* out_handle) { | ||
| if (!out_handle) return RAC_ERROR_INVALID_ARGUMENT; | ||
| rac_lora_registry* registry = new rac_lora_registry(); | ||
| RAC_LOG_INFO("LoraRegistry", "LoRA registry created"); | ||
| *out_handle = registry; | ||
| return RAC_SUCCESS; | ||
| } |
There was a problem hiding this comment.
new rac_lora_registry() can throw std::bad_alloc across the C boundary.
This is a extern "C" function, so an uncaught C++ exception results in std::terminate. Use new (std::nothrow) and check for nullptr to return RAC_ERROR_OUT_OF_MEMORY instead.
🐛 Proposed fix
rac_result_t rac_lora_registry_create(rac_lora_registry_handle_t* out_handle) {
if (!out_handle) return RAC_ERROR_INVALID_ARGUMENT;
- rac_lora_registry* registry = new rac_lora_registry();
+ rac_lora_registry* registry = new (std::nothrow) rac_lora_registry();
+ if (!registry) return RAC_ERROR_OUT_OF_MEMORY;
RAC_LOG_INFO("LoraRegistry", "LoRA registry created");
*out_handle = registry;
return RAC_SUCCESS;
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@sdk/runanywhere-commons/src/infrastructure/model_management/lora_registry.cpp`
around lines 65 - 71, The extern "C" function rac_lora_registry_create currently
uses operator new which can throw across the C boundary; replace new
rac_lora_registry() with a non-throwing allocation (use new(std::nothrow)
rac_lora_registry) and check the result for nullptr, logging an error and
returning RAC_ERROR_OUT_OF_MEMORY if allocation fails, otherwise assign to
*out_handle and return RAC_SUCCESS; ensure you still validate out_handle at the
start and keep the RAC_LOG_INFO call only on successful allocation.
| rac_result_t rac_lora_registry_register(rac_lora_registry_handle_t handle, | ||
| const rac_lora_entry_t* entry) { | ||
| if (!handle || !entry || !entry->id) return RAC_ERROR_INVALID_ARGUMENT; | ||
| std::lock_guard<std::mutex> lock(handle->mutex); | ||
| std::string adapter_id = entry->id; | ||
| auto it = handle->entries.find(adapter_id); | ||
| if (it != handle->entries.end()) { free_lora_entry(it->second); } | ||
| rac_lora_entry_t* copy = deep_copy_lora_entry(entry); | ||
| if (!copy) return RAC_ERROR_OUT_OF_MEMORY; | ||
| handle->entries[adapter_id] = copy; | ||
| RAC_LOG_DEBUG("LoraRegistry", "LoRA adapter registered: %s", entry->id); | ||
| return RAC_SUCCESS; | ||
| } |
There was a problem hiding this comment.
Dangling pointer in map on OOM during re-registration.
If adapter_id already exists, Line 89 frees the old entry's memory but the map key still holds the now-dangling pointer. If deep_copy_lora_entry then fails (Line 91), the function returns RAC_ERROR_OUT_OF_MEMORY leaving handle->entries[adapter_id] pointing to freed memory. Any subsequent get, get_all, get_for_model, or remove call will dereference the dangling pointer (UB / crash).
Fix: deep-copy before freeing the old entry so the map is never left in an inconsistent state.
🐛 Proposed fix
rac_result_t rac_lora_registry_register(rac_lora_registry_handle_t handle,
const rac_lora_entry_t* entry) {
if (!handle || !entry || !entry->id) return RAC_ERROR_INVALID_ARGUMENT;
std::lock_guard<std::mutex> lock(handle->mutex);
std::string adapter_id = entry->id;
+ rac_lora_entry_t* copy = deep_copy_lora_entry(entry);
+ if (!copy) return RAC_ERROR_OUT_OF_MEMORY;
auto it = handle->entries.find(adapter_id);
- if (it != handle->entries.end()) { free_lora_entry(it->second); }
- rac_lora_entry_t* copy = deep_copy_lora_entry(entry);
- if (!copy) return RAC_ERROR_OUT_OF_MEMORY;
+ if (it != handle->entries.end()) { free_lora_entry(it->second); }
handle->entries[adapter_id] = copy;
RAC_LOG_DEBUG("LoraRegistry", "LoRA adapter registered: %s", entry->id);
return RAC_SUCCESS;
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@sdk/runanywhere-commons/src/infrastructure/model_management/lora_registry.cpp`
around lines 83 - 95, The code in rac_lora_registry_register frees the existing
entry (free_lora_entry(it->second)) before calling deep_copy_lora_entry, which
can leave handle->entries[adapter_id] pointing at freed memory if the deep copy
fails; change the logic to perform deep_copy_lora_entry first into a temporary
pointer, check for NULL and return RAC_ERROR_OUT_OF_MEMORY if it fails, and only
then free the old entry and assign the new copy into handle->entries[adapter_id]
(all within the same std::lock_guard scope) so the map never contains a dangling
pointer; keep the existing return values and logging behavior in
rac_lora_registry_register.
| data class LoraEntry( | ||
| val id: String, | ||
| val name: String, | ||
| val description: String, | ||
| val downloadUrl: String, | ||
| val filename: String, | ||
| val compatibleModelIds: List<String>, | ||
| val fileSize: Long, | ||
| val defaultScale: Float, | ||
| ) | ||
|
|
||
| fun register(entry: LoraEntry) { | ||
| log(LogLevel.DEBUG, "Registering LoRA adapter: ${entry.id}") | ||
| val result = RunAnywhereBridge.racLoraRegistryRegister( | ||
| id = entry.id, name = entry.name, description = entry.description, | ||
| downloadUrl = entry.downloadUrl, filename = entry.filename, | ||
| compatibleModelIds = entry.compatibleModelIds.toTypedArray(), | ||
| fileSize = entry.fileSize, defaultScale = entry.defaultScale, | ||
| ) | ||
| if (result != RunAnywhereBridge.RAC_SUCCESS) { | ||
| log(LogLevel.ERROR, "Failed to register LoRA adapter: ${entry.id}, error=$result") | ||
| throw RuntimeException("Failed to register LoRA adapter: $result") | ||
| } | ||
| log(LogLevel.INFO, "LoRA adapter registered: ${entry.id}") | ||
| } | ||
|
|
||
| fun getForModel(modelId: String): List<LoraEntry> { | ||
| val json = RunAnywhereBridge.racLoraRegistryGetForModel(modelId) | ||
| return parseLoraEntryArrayJson(json) | ||
| } | ||
|
|
||
| fun getAll(): List<LoraEntry> { | ||
| val json = RunAnywhereBridge.racLoraRegistryGetAll() | ||
| return parseLoraEntryArrayJson(json) | ||
| } | ||
|
|
||
| // JSON Parsing | ||
|
|
||
| private fun parseLoraEntryJson(json: String): LoraEntry? { | ||
| if (json == "null" || json.isBlank()) return null | ||
| return try { | ||
| LoraEntry( | ||
| id = extractString(json, "id") ?: return null, | ||
| name = extractString(json, "name") ?: "", | ||
| description = extractString(json, "description") ?: "", | ||
| downloadUrl = extractString(json, "download_url") ?: "", | ||
| filename = extractString(json, "filename") ?: "", | ||
| compatibleModelIds = extractStringArray(json, "compatible_model_ids"), | ||
| fileSize = extractLong(json, "file_size"), | ||
| defaultScale = extractFloat(json, "default_scale"), | ||
| ) | ||
| } catch (e: Exception) { | ||
| log(LogLevel.ERROR, "Failed to parse LoRA entry JSON: ${e.message}") | ||
| null | ||
| } | ||
| } | ||
|
|
||
| private fun parseLoraEntryArrayJson(json: String): List<LoraEntry> { | ||
| if (json == "[]" || json.isBlank()) return emptyList() | ||
| val entries = mutableListOf<LoraEntry>() | ||
| var depth = 0; var objectStart = -1 | ||
| for (i in json.indices) { | ||
| when (json[i]) { | ||
| '{' -> { if (depth == 0) objectStart = i; depth++ } | ||
| '}' -> { | ||
| depth-- | ||
| if (depth == 0 && objectStart >= 0) { | ||
| parseLoraEntryJson(json.substring(objectStart, i + 1))?.let { entries.add(it) } | ||
| objectStart = -1 | ||
| } | ||
| } | ||
| } | ||
| } | ||
| return entries | ||
| } | ||
|
|
||
| private fun extractString(json: String, key: String): String? { | ||
| val regex = Regex(""""$key"\s*:\s*"((?:[^"\\]|\\.)*)"""") | ||
| return regex.find(json)?.groupValues?.get(1)?.takeIf { it.isNotEmpty() } | ||
| } | ||
|
|
||
| private fun extractLong(json: String, key: String): Long { | ||
| val regex = Regex(""""$key"\s*:\s*(-?\d+)""") | ||
| return regex.find(json)?.groupValues?.get(1)?.toLongOrNull() ?: 0L | ||
| } | ||
|
|
||
| private fun extractFloat(json: String, key: String): Float { | ||
| val regex = Regex(""""$key"\s*:\s*(-?[\d.]+)""") | ||
| return regex.find(json)?.groupValues?.get(1)?.toFloatOrNull() ?: 0f | ||
| } | ||
|
|
||
| private fun extractStringArray(json: String, key: String): List<String> { | ||
| val keyMatch = Regex(""""$key"\s*:\s*\[""").find(json) ?: return emptyList() | ||
| val arrayStart = keyMatch.range.last + 1 | ||
| var depth = 1; var pos = arrayStart | ||
| while (pos < json.length && depth > 0) { | ||
| when (json[pos]) { '[' -> depth++; ']' -> depth-- }; pos++ | ||
| } | ||
| if (depth != 0) return emptyList() | ||
| val arrayContent = json.substring(arrayStart, pos - 1).trim() | ||
| if (arrayContent.isEmpty()) return emptyList() | ||
| return Regex(""""((?:[^"\\]|\\.)*)"""").findAll(arrayContent).map { it.groupValues[1] }.toList() | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Move LoraEntry + JSON parsing into commonMain.
This file contains core data structures and parsing logic that belong in commonMain; keep only the JNI wiring in jvmAndroidMain.
As per coding guidelines: "Define all business logic, protocols, interfaces, and structures in commonMain/ rather than in platform-specific modules".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@sdk/runanywhere-kotlin/src/jvmAndroidMain/kotlin/com/runanywhere/sdk/foundation/bridge/extensions/CppBridgeLoraRegistry.kt`
around lines 16 - 118, The LoraEntry data class and JSON parsing helpers belong
in commonMain; move the LoraEntry definition plus parseLoraEntryJson,
parseLoraEntryArrayJson, extractString, extractLong, extractFloat, and
extractStringArray into the commonMain module (preserving their signatures), and
leave only the JNI-facing wiring and calls (register, getForModel, getAll) in
this jvmAndroidMain file so platform-specific code only invokes the common
logic; update imports/usages so RunAnywhereBridge calls the common parsing
functions and remove duplicate parsing code from the JNI file.
| init(from cEntry: rac_lora_entry_t) { | ||
| self.id = String(cString: cEntry.id) | ||
| self.name = String(cString: cEntry.name) | ||
| self.adapterDescription = String(cString: cEntry.description) | ||
| self.downloadURL = URL(string: String(cString: cEntry.download_url)) ?? URL(fileURLWithPath: "") | ||
| self.filename = String(cString: cEntry.filename) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find sdk -name "*.h" -o -name "*.hpp" | xargs grep -l "rac_lora_entry_t" 2>/dev/null | head -5Repository: RunanywhereAI/runanywhere-sdks
Length of output: 242
🏁 Script executed:
# Search for the struct definition
rg "typedef struct.*rac_lora_entry" sdk -A 20 -B 2Repository: RunanywhereAI/runanywhere-sdks
Length of output: 5863
🏁 Script executed:
# Look in commons directory for C header definitions
find sdk/runanywhere-commons -name "*.h" -type f 2>/dev/null | head -10Repository: RunanywhereAI/runanywhere-sdks
Length of output: 695
🏁 Script executed:
rg "rac_lora_entry_t" sdk/runanywhere-commons/src -B 2 -A 2 -g '*.{c,cpp,h}' | head -100Repository: RunanywhereAI/runanywhere-sdks
Length of output: 10476
🏁 Script executed:
# Search for where entry fields are assigned
rg "\.id\s*=" sdk/runanywhere-commons/src -B 3 -A 1 -g '*.{c,cpp}' | head -80Repository: RunanywhereAI/runanywhere-sdks
Length of output: 6908
🏁 Script executed:
# Look at the registry API that returns entries
rg "rac_lora_registry_get" sdk/runanywhere-commons -A 10 -g '*.h'Repository: RunanywhereAI/runanywhere-sdks
Length of output: 4615
🏁 Script executed:
rg "rac_lora_registry" sdk/runanywhere-swift/Sources -B 2 -A 5 -g '*.swift' | head -120Repository: RunanywhereAI/runanywhere-sdks
Length of output: 5754
🏁 Script executed:
# Look for the actual init(from:) function to understand the full context
sed -n '480,510p' sdk/runanywhere-swift/Sources/RunAnywhere/Public/Extensions/LLM/LLMTypes.swiftRepository: RunanywhereAI/runanywhere-sdks
Length of output: 1221
🏁 Script executed:
# Check if there are null checks elsewhere before calling init(from:)
rg "init\(from: rac_lora_entry" sdk/runanywhere-swift/Sources -B 5 -A 10 -g '*.swift'Repository: RunanywhereAI/runanywhere-sdks
Length of output: 56
Guard against null C strings in LoraAdapterCatalogEntry initializer.
String(cString:) crashes on null pointers. The C API allows null values in string fields (evidenced by JNI code setting entry.id = id_str ? strdup(id_str) : nullptr), but this initializer doesn't check for them before constructing strings.
🛠️ Safer conversion
init(from cEntry: rac_lora_entry_t) {
- self.id = String(cString: cEntry.id)
- self.name = String(cString: cEntry.name)
- self.adapterDescription = String(cString: cEntry.description)
- self.downloadURL = URL(string: String(cString: cEntry.download_url)) ?? URL(fileURLWithPath: "")
- self.filename = String(cString: cEntry.filename)
+ func cString(_ ptr: UnsafePointer<CChar>?) -> String {
+ ptr.map { String(cString: $0) } ?? ""
+ }
+ self.id = cString(cEntry.id)
+ self.name = cString(cEntry.name)
+ self.adapterDescription = cString(cEntry.description)
+ let downloadUrlString = cString(cEntry.download_url)
+ self.downloadURL = URL(string: downloadUrlString) ?? URL(fileURLWithPath: "")
+ self.filename = cString(cEntry.filename)
var modelIds: [String] = []📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| init(from cEntry: rac_lora_entry_t) { | |
| self.id = String(cString: cEntry.id) | |
| self.name = String(cString: cEntry.name) | |
| self.adapterDescription = String(cString: cEntry.description) | |
| self.downloadURL = URL(string: String(cString: cEntry.download_url)) ?? URL(fileURLWithPath: "") | |
| self.filename = String(cString: cEntry.filename) | |
| init(from cEntry: rac_lora_entry_t) { | |
| func cString(_ ptr: UnsafePointer<CChar>?) -> String { | |
| ptr.map { String(cString: $0) } ?? "" | |
| } | |
| self.id = cString(cEntry.id) | |
| self.name = cString(cEntry.name) | |
| self.adapterDescription = cString(cEntry.description) | |
| let downloadUrlString = cString(cEntry.download_url) | |
| self.downloadURL = URL(string: downloadUrlString) ?? URL(fileURLWithPath: "") | |
| self.filename = cString(cEntry.filename) | |
| var modelIds: [String] = [] |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@sdk/runanywhere-swift/Sources/RunAnywhere/Public/Extensions/LLM/LLMTypes.swift`
around lines 485 - 490, The LoraAdapterCatalogEntry initializer init(from
cEntry: rac_lora_entry_t) uses String(cString:) on C pointers that can be null,
which will crash; update the initializer to check each C string pointer
(cEntry.id, cEntry.name, cEntry.description, cEntry.download_url,
cEntry.filename) for nil before converting and assign a safe default (empty
string or make the Swift properties optional) and construct downloadURL only if
download_url is non-nil and produces a valid URL, otherwise fall back to a safe
URL or nil; adjust property types if you choose optionals and ensure all
references to LoraAdapterCatalogEntry match the new null-safe semantics.
- LoraViewModel: add ensureActive() in download loop so coroutine cancellation is respected during blocking stream reads - lora_registry: check all rac_strdup results in deep_copy_lora_entry to prevent silent partial copies on OOM; use calloc for compatible_model_ids array and set count before fill loop so free_lora_entry can safely clean up on partial failure Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
@coderabbitai please review |
|
@greptile please review |
|
✅ Actions performedReview triggered.
|
| val connection = URL(entry.downloadUrl).openConnection().apply { | ||
| connectTimeout = 30_000 | ||
| readTimeout = 60_000 | ||
| } | ||
| connection.connect() |
There was a problem hiding this comment.
HTTP downloads allowed without scheme validation — MITM attack vector.
URL(entry.downloadUrl).openConnection() accepts any scheme. If a catalog entry has an http:// URL, the adapter downloads unencrypted, allowing attackers to inject malicious model files.
| val connection = URL(entry.downloadUrl).openConnection().apply { | |
| connectTimeout = 30_000 | |
| readTimeout = 60_000 | |
| } | |
| connection.connect() | |
| val url = URL(entry.downloadUrl) | |
| require(url.protocol == "https") { "Only HTTPS downloads are allowed for security" } | |
| val connection = url.openConnection().apply { |
Prompt To Fix With AI
This is a comment left during a code review.
Path: examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/lora/LoraViewModel.kt
Line: 202-206
Comment:
HTTP downloads allowed without scheme validation — MITM attack vector.
`URL(entry.downloadUrl).openConnection()` accepts any scheme. If a catalog entry has an `http://` URL, the adapter downloads unencrypted, allowing attackers to inject malicious model files.
```suggestion
val url = URL(entry.downloadUrl)
require(url.protocol == "https") { "Only HTTPS downloads are allowed for security" }
val connection = url.openConnection().apply {
```
How can I resolve this? If you propose a fix, please make it concise.| } | ||
| destFile.delete() | ||
| if (!tmpFile.renameTo(destFile)) { | ||
| tmpFile.delete() | ||
| throw Exception("Failed to move downloaded file to final location") | ||
| } | ||
| downloadComplete = true |
There was a problem hiding this comment.
Downloaded file size never validated against entry.fileSize.
Truncated or corrupted downloads silently rename to final destination. Loading corrupt adapters will crash the model or produce garbage output.
After the download loop completes, add:
if (downloaded != totalSize) {
throw Exception("Download incomplete: got $downloaded bytes, expected $totalSize")
}Prompt To Fix With AI
This is a comment left during a code review.
Path: examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/lora/LoraViewModel.kt
Line: 224-230
Comment:
Downloaded file size never validated against `entry.fileSize`.
Truncated or corrupted downloads silently rename to final destination. Loading corrupt adapters will crash the model or produce garbage output.
After the download loop completes, add:
```kotlin
if (downloaded != totalSize) {
throw Exception("Download incomplete: got $downloaded bytes, expected $totalSize")
}
```
How can I resolve this? If you propose a fix, please make it concise.| LoraAdapterCatalogEntry( | ||
| id = "chat-assistant-lora", | ||
| name = "Chat Assistant", | ||
| description = "Enhances conversational chat ability", | ||
| downloadUrl = "https://huggingface.co/Void2377/Qwen/resolve/main/lora/chat_assistant-lora-Q8_0.gguf", | ||
| filename = "chat_assistant-lora-Q8_0.gguf", | ||
| compatibleModelIds = listOf("lfm2-350m-q4_k_m", "lfm2-350m-q8_0"), | ||
| fileSize = 690_176, | ||
| defaultScale = 1.0f, | ||
| ), |
There was a problem hiding this comment.
Qwen-trained LoRA marked compatible with LFM2-350M — architecture mismatch.
These LoRA files from Void2377/Qwen are described as Qwen adapters. LoRA weights are architecture-specific. Applying Qwen-trained LoRAs to LiquidAI LFM2 models will produce nonsensical output or crash.
Check that these were actually trained for LFM2-350M. If placeholders for demo, add a comment clarifying they're examples only.
Prompt To Fix With AI
This is a comment left during a code review.
Path: examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/data/ModelList.kt
Line: 86-95
Comment:
Qwen-trained LoRA marked compatible with LFM2-350M — architecture mismatch.
These LoRA files from `Void2377/Qwen` are described as Qwen adapters. LoRA weights are architecture-specific. Applying Qwen-trained LoRAs to LiquidAI LFM2 models will produce nonsensical output or crash.
Check that these were actually trained for LFM2-350M. If placeholders for demo, add a comment clarifying they're examples only.
How can I resolve this? If you propose a fix, please make it concise.Resolve conflicts: - Package.swift: keep main's split ONNX Runtime xcframeworks (iOS + macOS) - project.pbxproj: keep both RunAnywhereRAG and RunAnywhereWhisperKit dependencies Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
Actionable comments posted: 4
♻️ Duplicate comments (4)
examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/lora/LoraViewModel.kt (1)
281-283: Checkdelete()return value and log failures.
file.delete()can fail silently (permissions, open handles). Log a warning or surface a user error if it returnsfalse.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/lora/LoraViewModel.kt` around lines 281 - 283, The current deletion in LoraViewModel calls file.delete() and always logs success via Timber.i; change it to check the boolean return of file.delete() and handle failures: if delete() returns true, log the existing success message (Timber.i("Deleted LoRA adapter file: ${entry.filename}")), otherwise log a warning or surface an error to the user (e.g., Timber.w or emit an error event) including entry.filename and file.path so failures (permissions/open handles) are visible for troubleshooting. Ensure this check replaces the unconditional delete + log and is used wherever file.delete() is called for LoRA adapter cleanup.sdk/runanywhere-commons/src/infrastructure/model_management/lora_registry.cpp (3)
102-114:⚠️ Potential issue | 🔴 CriticalDangling pointer in the registry map when
deep_copy_lora_entryfails during re-registration.Line 108 frees
it->secondbut does not erase the map entry. Ifdeep_copy_lora_entry(line 109) then returnsnullptr,handle->entries[adapter_id]still holds the freed (dangling) pointer. Any subsequentget,get_all,get_for_model, orremovecall will dereference it — undefined behaviour / crash.Fix: deep-copy first, only free the old entry once the new copy is confirmed valid.
🐛 Proposed fix
rac_result_t rac_lora_registry_register(rac_lora_registry_handle_t handle, const rac_lora_entry_t* entry) { if (!handle || !entry || !entry->id) return RAC_ERROR_INVALID_ARGUMENT; std::lock_guard<std::mutex> lock(handle->mutex); std::string adapter_id = entry->id; + rac_lora_entry_t* copy = deep_copy_lora_entry(entry); + if (!copy) return RAC_ERROR_OUT_OF_MEMORY; auto it = handle->entries.find(adapter_id); - if (it != handle->entries.end()) { free_lora_entry(it->second); } - rac_lora_entry_t* copy = deep_copy_lora_entry(entry); - if (!copy) return RAC_ERROR_OUT_OF_MEMORY; + if (it != handle->entries.end()) { free_lora_entry(it->second); } handle->entries[adapter_id] = copy; RAC_LOG_DEBUG("LoraRegistry", "LoRA adapter registered: %s", entry->id); return RAC_SUCCESS; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-commons/src/infrastructure/model_management/lora_registry.cpp` around lines 102 - 114, In rac_lora_registry_register, the code frees the existing entry (it->second) before performing deep_copy_lora_entry, which can return nullptr and leave a dangling pointer in handle->entries; change the flow to call deep_copy_lora_entry(entry) first, check the returned copy for nullptr, and only after confirming success, acquire the lock (or use the existing lock_guard), free the old it->second and then assign handle->entries[adapter_id] = copy; ensure you still return RAC_ERROR_OUT_OF_MEMORY if deep_copy_lora_entry fails and keep the RAC_LOG_DEBUG on success.
84-90:⚠️ Potential issue | 🟠 Major
operator newcan throwstd::bad_allocacross the C ABI boundary.
rac_lora_registry_createis part of anextern "C"surface; an uncaught C++ exception here causesstd::terminate. Usenew (std::nothrow)and returnRAC_ERROR_OUT_OF_MEMORYon failure.🐛 Proposed fix
rac_result_t rac_lora_registry_create(rac_lora_registry_handle_t* out_handle) { if (!out_handle) return RAC_ERROR_INVALID_ARGUMENT; - rac_lora_registry* registry = new rac_lora_registry(); + rac_lora_registry* registry = new (std::nothrow) rac_lora_registry(); + if (!registry) return RAC_ERROR_OUT_OF_MEMORY; RAC_LOG_INFO("LoraRegistry", "LoRA registry created"); *out_handle = registry; return RAC_SUCCESS; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-commons/src/infrastructure/model_management/lora_registry.cpp` around lines 84 - 90, The function rac_lora_registry_create may throw std::bad_alloc when using operator new across the C ABI; change allocation in rac_lora_registry_create to use nothrow new (e.g., new (std::nothrow) rac_lora_registry()), check the returned pointer for null, set *out_handle = nullptr on failure, log an error with context (e.g., "Failed to create LoRA registry: out of memory"), and return RAC_ERROR_OUT_OF_MEMORY instead of letting an exception propagate; keep the existing RAC_SUCCESS path when allocation succeeds.
19-21: 🛠️ Refactor suggestion | 🟠 MajorUse a structured ID type for the registry map key instead of raw
std::string.
std::map<std::string, rac_lora_entry_t*>uses a raw string as key, also echoed in function-localstd::string adapter_idat line 106. As per coding guidelines, "Always use structured types, never use strings directly for consistency and scalability."🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-commons/src/infrastructure/model_management/lora_registry.cpp` around lines 19 - 21, The registry uses raw std::string keys (rac_lora_registry::entries and local adapter_id) which violates the guideline to use structured ID types; replace the string key with a dedicated ID type (e.g., a small struct or typedef such as LoraAdapterId) and update rac_lora_registry::entries to std::map<LoraAdapterId, rac_lora_entry_t*>, then change all usages including the function-local adapter_id and any map lookups, insertions, comparisons, and constructor/serialization helpers to use the new LoraAdapterId type (implement comparison/operator< or provide a custom comparator) so code referencing rac_lora_registry, entries, rac_lora_entry_t and adapter_id compiles and follows the structured-ID convention.
🧹 Nitpick comments (2)
examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/lora/LoraViewModel.kt (2)
45-45: Rename Android-specific ViewModel to include theAndroidprefix.This file is under an Android-specific path but the class name lacks the
Androidprefix (e.g.,AndroidLoraViewModel).
As per coding guidelines, "Name Android-specific implementations with clear 'Android' prefix (e.g., 'AndroidTTSService.kt' not 'TTSService.kt' or 'TTSServiceImpl.kt')".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/lora/LoraViewModel.kt` at line 45, Rename the Android-specific ViewModel class LoraViewModel to AndroidLoraViewModel across the module: update the class declaration (class LoraViewModel(...) -> class AndroidLoraViewModel(...)), rename the Kotlin file accordingly, and update all references/usages (instantiations, ViewModelProvider keys, DI bindings, imports, and tests) to the new AndroidLoraViewModel symbol to keep naming consistent with the Android-specific implementation convention.
31-39: Model errors as a sealed/structured type, not a raw String.
LoraUiState.errorstores a String and all catches build ad‑hoc messages. This loses type information and makes error handling brittle across the UI. Consider a sealedLoraError(with user‑facing formatting in UI) and store that in state instead.
As per coding guidelines, "Always use structured types and data classes instead of strings directly for type safety and consistency" and "Use sealed classes for type-safe error handling rather than generic exception handling".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/lora/LoraViewModel.kt` around lines 31 - 39, Replace the raw String error field in LoraUiState with a sealed/structured error type and propagate it through the ViewModel and UI: introduce a sealed class (e.g., LoraError with variants like NetworkError(message), DownloadError(id, message), UnknownError(cause)) and change LoraUiState.error: String? to error: LoraError?; update all places that set or read LoraUiState.error (in the ViewModel and any callers) to construct appropriate LoraError variants instead of ad‑hoc strings, and adapt UI formatting to render user‑facing messages from LoraError instances.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/lora/LoraViewModel.kt`:
- Around line 176-181: scanDownloadedAdapters (and any download/delete handlers)
currently constructs File(loraDir, entry.filename) directly, allowing path
traversal via filenames like "../". Before using the File, resolve its
canonical/real path (e.g., file.canonicalPath or file.toPath().toRealPath()) and
the canonical path of loraDir, then verify the resolved file path startsWith the
resolved loraDir path; if it does not, treat the entry as invalid and skip or
reject it. Apply the same canonicalization and containment check in any related
methods that write or delete adapter files to ensure all file operations remain
strictly inside loraDir.
- Around line 185-196: downloadAdapter can race because the check of
_uiState.value.downloadingAdapterId and the subsequent _uiState.update are not
atomic, allowing concurrent calls to start multiple downloadJob coroutines;
protect download initiation by adding a guard (e.g., a private Mutex or an
AtomicBoolean like isDownloadStarting) in the LoraViewModel and wrap the
check-and-update/start sequence inside a single lock or an atomic
compare-and-set so only one caller can proceed to set downloadingAdapterId and
launch downloadJob; update references to downloadAdapter, downloadJob, and
_uiState accordingly to acquire/release the guard around the early-return check,
the _uiState.update, and the viewModelScope.launch call.
In `@sdk/runanywhere-commons/include/rac/core/rac_core.h`:
- Around line 343-350: The doc comment for rac_get_lora_registry is ambiguous
about NULL returns; update it to explicitly state the contract: specify whether
the function may return NULL on allocation failure (e.g., "may return NULL if
lazy initialization/allocation fails") and instruct callers to handle that case,
or change wording to match rac_get_model_registry by omitting NULL wording if
you intend it never to return NULL (and ensure implementation aborts/logs on
allocation failure). Modify the comment for rac_get_lora_registry to clearly
state the lazy-init behavior and the exact NULL semantics, referencing
rac_get_model_registry as the stylistic precedent.
In
`@sdk/runanywhere-commons/src/infrastructure/model_management/lora_registry.cpp`:
- Around line 24-64: deep_copy_lora_entry calls free_lora_entry before that
static function is defined, causing a compile error; add a forward declaration
for free_lora_entry (matching its real signature, e.g. static void
free_lora_entry(rac_lora_entry_t*);) placed above deep_copy_lora_entry so calls
at lines where deep_copy_lora_entry invokes free_lora_entry will resolve.
---
Duplicate comments:
In
`@examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/lora/LoraViewModel.kt`:
- Around line 281-283: The current deletion in LoraViewModel calls file.delete()
and always logs success via Timber.i; change it to check the boolean return of
file.delete() and handle failures: if delete() returns true, log the existing
success message (Timber.i("Deleted LoRA adapter file: ${entry.filename}")),
otherwise log a warning or surface an error to the user (e.g., Timber.w or emit
an error event) including entry.filename and file.path so failures
(permissions/open handles) are visible for troubleshooting. Ensure this check
replaces the unconditional delete + log and is used wherever file.delete() is
called for LoRA adapter cleanup.
In
`@sdk/runanywhere-commons/src/infrastructure/model_management/lora_registry.cpp`:
- Around line 102-114: In rac_lora_registry_register, the code frees the
existing entry (it->second) before performing deep_copy_lora_entry, which can
return nullptr and leave a dangling pointer in handle->entries; change the flow
to call deep_copy_lora_entry(entry) first, check the returned copy for nullptr,
and only after confirming success, acquire the lock (or use the existing
lock_guard), free the old it->second and then assign handle->entries[adapter_id]
= copy; ensure you still return RAC_ERROR_OUT_OF_MEMORY if deep_copy_lora_entry
fails and keep the RAC_LOG_DEBUG on success.
- Around line 84-90: The function rac_lora_registry_create may throw
std::bad_alloc when using operator new across the C ABI; change allocation in
rac_lora_registry_create to use nothrow new (e.g., new (std::nothrow)
rac_lora_registry()), check the returned pointer for null, set *out_handle =
nullptr on failure, log an error with context (e.g., "Failed to create LoRA
registry: out of memory"), and return RAC_ERROR_OUT_OF_MEMORY instead of letting
an exception propagate; keep the existing RAC_SUCCESS path when allocation
succeeds.
- Around line 19-21: The registry uses raw std::string keys
(rac_lora_registry::entries and local adapter_id) which violates the guideline
to use structured ID types; replace the string key with a dedicated ID type
(e.g., a small struct or typedef such as LoraAdapterId) and update
rac_lora_registry::entries to std::map<LoraAdapterId, rac_lora_entry_t*>, then
change all usages including the function-local adapter_id and any map lookups,
insertions, comparisons, and constructor/serialization helpers to use the new
LoraAdapterId type (implement comparison/operator< or provide a custom
comparator) so code referencing rac_lora_registry, entries, rac_lora_entry_t and
adapter_id compiles and follows the structured-ID convention.
---
Nitpick comments:
In
`@examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/lora/LoraViewModel.kt`:
- Line 45: Rename the Android-specific ViewModel class LoraViewModel to
AndroidLoraViewModel across the module: update the class declaration (class
LoraViewModel(...) -> class AndroidLoraViewModel(...)), rename the Kotlin file
accordingly, and update all references/usages (instantiations, ViewModelProvider
keys, DI bindings, imports, and tests) to the new AndroidLoraViewModel symbol to
keep naming consistent with the Android-specific implementation convention.
- Around line 31-39: Replace the raw String error field in LoraUiState with a
sealed/structured error type and propagate it through the ViewModel and UI:
introduce a sealed class (e.g., LoraError with variants like
NetworkError(message), DownloadError(id, message), UnknownError(cause)) and
change LoraUiState.error: String? to error: LoraError?; update all places that
set or read LoraUiState.error (in the ViewModel and any callers) to construct
appropriate LoraError variants instead of ad‑hoc strings, and adapt UI
formatting to render user‑facing messages from LoraError instances.
ℹ️ Review info
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/lora/LoraAdapterPickerSheet.ktexamples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/lora/LoraViewModel.ktsdk/runanywhere-commons/include/rac/core/rac_core.hsdk/runanywhere-commons/src/infrastructure/model_management/lora_registry.cpp
| /** Scan disk for downloaded adapter files and return id->path map. Must be called on IO dispatcher. */ | ||
| private fun scanDownloadedAdapters(adapters: List<LoraAdapterCatalogEntry>): Map<String, String> { | ||
| return adapters.mapNotNull { entry -> | ||
| val file = File(loraDir, entry.filename) | ||
| if (file.exists()) entry.id to file.absolutePath else null | ||
| }.toMap() |
There was a problem hiding this comment.
Validate adapter filenames to prevent path traversal.
File(loraDir, entry.filename) trusts catalog filenames. If a catalog entry contains ../, files could be written/read outside loraDir. Please canonicalize and enforce that resolved paths stay under loraDir before any file operation (scan/download/delete).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/lora/LoraViewModel.kt`
around lines 176 - 181, scanDownloadedAdapters (and any download/delete
handlers) currently constructs File(loraDir, entry.filename) directly, allowing
path traversal via filenames like "../". Before using the File, resolve its
canonical/real path (e.g., file.canonicalPath or file.toPath().toRealPath()) and
the canonical path of loraDir, then verify the resolved file path startsWith the
resolved loraDir path; if it does not, treat the entry as invalid and skip or
reject it. Apply the same canonicalization and containment check in any related
methods that write or delete adapter files to ensure all file operations remain
strictly inside loraDir.
| fun downloadAdapter(entry: LoraAdapterCatalogEntry) { | ||
| if (_uiState.value.downloadingAdapterId != null) return | ||
|
|
||
| _uiState.update { | ||
| it.copy( | ||
| downloadingAdapterId = entry.id, | ||
| downloadProgress = 0f, | ||
| error = null, | ||
| ) | ||
| } | ||
|
|
||
| downloadJob = viewModelScope.launch { |
There was a problem hiding this comment.
Guard against concurrent download starts.
The downloadingAdapterId check is not atomic with the update; concurrent calls could still start multiple downloads. Consider a Mutex/AtomicBoolean guard or a single-threaded dispatcher for download initiation to ensure only one job starts.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/lora/LoraViewModel.kt`
around lines 185 - 196, downloadAdapter can race because the check of
_uiState.value.downloadingAdapterId and the subsequent _uiState.update are not
atomic, allowing concurrent calls to start multiple downloadJob coroutines;
protect download initiation by adding a guard (e.g., a private Mutex or an
AtomicBoolean like isDownloadStarting) in the LoraViewModel and wrap the
check-and-update/start sequence inside a single lock or an atomic
compare-and-set so only one caller can proceed to set downloadingAdapterId and
launch downloadJob; update references to downloadAdapter, downloadJob, and
_uiState accordingly to acquire/release the guard around the early-return check,
the _uiState.update, and the viewModelScope.launch call.
| /** | ||
| * @brief Get the global LoRA adapter registry singleton | ||
| * | ||
| * The registry is lazily created on first access and lives for the process lifetime. | ||
| * | ||
| * @return Handle to the global registry (never NULL after first successful call) | ||
| */ | ||
| RAC_API struct rac_lora_registry* rac_get_lora_registry(void); |
There was a problem hiding this comment.
Clarify NULL-return contract for rac_get_lora_registry.
"Never NULL after first successful call" implies the function CAN return NULL (e.g., on first-ever call if lazy-init allocation fails), but callers are given no guidance on what to do in that case. Either document the return-NULL semantics explicitly, or harmonize with rac_get_model_registry (which simply says "Handle to the global model registry" and is documented as having lazy init elsewhere).
📄 Suggested documentation fix
-/**
- * `@brief` Get the global LoRA adapter registry singleton
- *
- * The registry is lazily created on first access and lives for the process lifetime.
- *
- * `@return` Handle to the global registry (never NULL after first successful call)
- */
+/**
+ * `@brief` Get the global LoRA adapter registry singleton.
+ *
+ * The registry is lazily created on first access and lives for the process
+ * lifetime. Returns NULL only if the internal allocation fails on the very
+ * first call; all subsequent calls return the same non-NULL handle.
+ * Callers must check for NULL before use.
+ *
+ * `@return` Handle to the global LoRA registry, or NULL if initial creation failed
+ */🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@sdk/runanywhere-commons/include/rac/core/rac_core.h` around lines 343 - 350,
The doc comment for rac_get_lora_registry is ambiguous about NULL returns;
update it to explicitly state the contract: specify whether the function may
return NULL on allocation failure (e.g., "may return NULL if lazy
initialization/allocation fails") and instruct callers to handle that case, or
change wording to match rac_get_model_registry by omitting NULL wording if you
intend it never to return NULL (and ensure implementation aborts/logs on
allocation failure). Modify the comment for rac_get_lora_registry to clearly
state the lazy-init behavior and the exact NULL semantics, referencing
rac_get_model_registry as the stylistic precedent.
sdk/runanywhere-commons/src/infrastructure/model_management/lora_registry.cpp
Show resolved
Hide resolved
There was a problem hiding this comment.
Actionable comments posted: 3
♻️ Duplicate comments (7)
sdk/runanywhere-commons/src/infrastructure/model_management/lora_registry.cpp (4)
102-114:⚠️ Potential issue | 🔴 CriticalDangling pointer in map when
deep_copy_lora_entryfails on re-registration.On re-registration of an existing adapter, line 108 frees the old entry while the map key still points to that memory. If
deep_copy_lora_entryat line 109 then returnsnullptr(OOM), the function returnsRAC_ERROR_OUT_OF_MEMORYbuthandle->entries[adapter_id]is left holding a dangling pointer. Any subsequentget,get_all,get_for_model, orremovecall will dereference it — undefined behaviour / crash.Fix: deep-copy first, then free the old entry only on success.
🐛 Proposed fix
rac_result_t rac_lora_registry_register(rac_lora_registry_handle_t handle, const rac_lora_entry_t* entry) { if (!handle || !entry || !entry->id) return RAC_ERROR_INVALID_ARGUMENT; std::lock_guard<std::mutex> lock(handle->mutex); std::string adapter_id = entry->id; + rac_lora_entry_t* copy = deep_copy_lora_entry(entry); + if (!copy) return RAC_ERROR_OUT_OF_MEMORY; auto it = handle->entries.find(adapter_id); - if (it != handle->entries.end()) { free_lora_entry(it->second); } - rac_lora_entry_t* copy = deep_copy_lora_entry(entry); - if (!copy) return RAC_ERROR_OUT_OF_MEMORY; + if (it != handle->entries.end()) { free_lora_entry(it->second); } handle->entries[adapter_id] = copy; RAC_LOG_DEBUG("LoraRegistry", "LoRA adapter registered: %s", entry->id); return RAC_SUCCESS; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-commons/src/infrastructure/model_management/lora_registry.cpp` around lines 102 - 114, In rac_lora_registry_register, avoid freeing the existing entry before you successfully create a deep copy: call deep_copy_lora_entry(entry) first and check for nullptr (handle OOM) and only after a successful copy call free_lora_entry on the old pointer and update handle->entries[adapter_id] = copy; ensure you still hold the lock (std::lock_guard<std::mutex> lock(handle->mutex)) while checking/updating the map and keep the RAC_LOG_DEBUG and return paths unchanged.
24-64:⚠️ Potential issue | 🔴 Critical
free_lora_entryis used before it is defined — confirmed compile error.
deep_copy_lora_entrycallsfree_lora_entryat lines 40, 48, and 56 for OOM cleanup.free_lora_entryis astaticfunction first defined at line 66. In C++ there is no implicit declaration; calling an undeclaredstaticfunction is a compile error, and the pipeline failure confirms this: "use of undeclared identifier 'free_lora_entry'".Add a forward declaration immediately before
deep_copy_lora_entry:🐛 Proposed fix
+// Forward declaration required by deep_copy_lora_entry for OOM cleanup +static void free_lora_entry(rac_lora_entry_t* entry); + static rac_lora_entry_t* deep_copy_lora_entry(const rac_lora_entry_t* src) {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-commons/src/infrastructure/model_management/lora_registry.cpp` around lines 24 - 64, The function deep_copy_lora_entry calls free_lora_entry before its definition, causing a compile error; add a forward declaration for free_lora_entry (matching its static signature) immediately above deep_copy_lora_entry so the compiler knows the symbol prior to use, then rebuild to confirm the error is resolved (refer to deep_copy_lora_entry and free_lora_entry).
19-21: Rawstd::stringmap keys violate the structured-types guideline.The registry stores and matches adapter/model IDs as raw
std::strings. Introduce a typed ID wrapper (or at minimum atypedef/usingalias) used consistently as map keys and across all API parameters, to align with the project's "always use structured types, never use strings directly" rule.As per coding guidelines: "Always use structured types, never use strings directly for consistency and scalability."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-commons/src/infrastructure/model_management/lora_registry.cpp` around lines 19 - 21, The registry uses raw std::string keys (entries in struct rac_lora_registry) which violates the "use structured types" rule; introduce a typed ID (e.g., a small wrapper type or a using/typedef such as LoraId) and replace the map key type in rac_lora_registry::entries from std::string to that new LoraId type, then update all API signatures and uses that take or compare adapter/model IDs (including functions that construct, lookup, insert, or erase rac_lora_entry_t entries) to accept and use LoraId instead of std::string so the project consistently uses the structured ID type.
84-90:⚠️ Potential issue | 🟠 Major
new rac_lora_registry()can throwstd::bad_allocacross the C boundary.
rac_lora_registry_createis called from C-facing code. An uncaughtstd::bad_alloccrossing anextern "C"boundary invokesstd::terminate. Usenew (std::nothrow)and returnRAC_ERROR_OUT_OF_MEMORYon failure, consistent with how other allocation failures are handled throughout this file.🐛 Proposed fix
rac_result_t rac_lora_registry_create(rac_lora_registry_handle_t* out_handle) { if (!out_handle) return RAC_ERROR_INVALID_ARGUMENT; - rac_lora_registry* registry = new rac_lora_registry(); + rac_lora_registry* registry = new (std::nothrow) rac_lora_registry(); + if (!registry) return RAC_ERROR_OUT_OF_MEMORY; RAC_LOG_INFO("LoraRegistry", "LoRA registry created"); *out_handle = registry; return RAC_SUCCESS; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-commons/src/infrastructure/model_management/lora_registry.cpp` around lines 84 - 90, The function rac_lora_registry_create uses plain new which can throw std::bad_alloc across the C boundary; change allocation to use new (std::nothrow) when constructing rac_lora_registry, check the returned pointer for nullptr, and if null return RAC_ERROR_OUT_OF_MEMORY (and optionally log an error) instead of allowing an exception to propagate; update the code paths that assign *out_handle and return RAC_SUCCESS only when the allocation succeeded (symbol: rac_lora_registry_create and type rac_lora_registry).sdk/runanywhere-commons/include/rac/core/rac_core.h (1)
343-350:⚠️ Potential issue | 🟡 MinorAmbiguous NULL-return contract on
rac_get_lora_registrystill unresolved.The phrase "never NULL after first successful call" implies the very first invocation can return
NULL(e.g., on allocation failure), but callers are given no guidance on what to do in that case. Contrast withrac_get_model_registryat line 307, which simply states "The registry is created automatically on first access" without the ambiguous qualifier.Either explicitly state that callers must check for
NULLand treat it as an initialization failure, or remove the qualifier if the implementation never returnsNULL(e.g., if it callsstd::aborton OOM).📄 Suggested doc fix
-/** - * `@brief` Get the global LoRA adapter registry singleton - * - * The registry is lazily created on first access and lives for the process lifetime. - * - * `@return` Handle to the global registry (never NULL after first successful call) - */ +/** + * `@brief` Get the global LoRA adapter registry singleton. + * + * The registry is lazily created on first access and lives for the process + * lifetime. May return NULL on the very first call if internal allocation + * fails. All subsequent calls after a successful init return the same + * non-NULL handle. Callers must check for NULL before use. + * + * `@return` Handle to the global LoRA registry, or NULL if initial creation failed + */Based on learnings: "Public C API headers in include/rac/ must document vtable operations, error codes, and lifecycle requirements."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-commons/include/rac/core/rac_core.h` around lines 343 - 350, The doc for rac_get_lora_registry is ambiguous about NULL returns; update the comment for rac_get_lora_registry to match rac_get_model_registry's clarity by either (A) explicitly stating "The registry is created automatically on first access and this function never returns NULL; on allocation failure the process aborts" if the implementation aborts on OOM, or (B) explicitly stating "Callers must check the return value for NULL; NULL indicates registry initialization failure (e.g., OOM) and should be handled as an error" if the implementation can return NULL; reference rac_get_lora_registry and rac_get_model_registry when making the change so the contract is consistent and documents lifecycle and error handling expectations.examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/lora/LoraViewModel.kt (2)
176-182:⚠️ Potential issue | 🟠 MajorValidate catalog filenames to prevent path traversal
File(loraDir, entry.filename)trusts catalog input; a filename like../can escape the adapter directory. Canonicalize and ensure the resolved path stays underloraDirbefore any file access (also apply this guard to download/delete).🛡️ Proposed fix
private fun scanDownloadedAdapters(adapters: List<LoraAdapterCatalogEntry>): Map<String, String> { - return adapters.mapNotNull { entry -> - val file = File(loraDir, entry.filename) - if (file.exists()) entry.id to file.absolutePath else null - }.toMap() + val root = loraDir.canonicalFile + return adapters.mapNotNull { entry -> + val file = File(root, entry.filename).canonicalFile + if (!file.path.startsWith(root.path + File.separator)) return@mapNotNull null + if (file.exists()) entry.id to file.absolutePath else null + }.toMap() }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/lora/LoraViewModel.kt` around lines 176 - 182, scanDownloadedAdapters currently constructs File(loraDir, entry.filename) directly and can be exploited via path traversal (e.g., "../") — canonicalize and validate resolved paths before using them: in scanDownloadedAdapters (and similarly in adapter download and delete functions), resolve the candidate file's canonical/absolute path and ensure it startsWith the canonical path of loraDir; if validation fails, skip or reject the entry and do not access the file. Also normalize input (reject empty or absolute filenames) and log or surface a clear error for invalid filenames.
185-196:⚠️ Potential issue | 🟡 MinorGuard against concurrent download starts
The
downloadingAdapterIdcheck isn’t atomic; concurrent calls can still start multiple downloads. Add an atomic guard or mutex around the check-and-start sequence.♻️ Example with AtomicBoolean
+import java.util.concurrent.atomic.AtomicBoolean ... + private val downloadInProgress = AtomicBoolean(false) ... - if (_uiState.value.downloadingAdapterId != null) return + if (!downloadInProgress.compareAndSet(false, true)) return ... - downloadJob = viewModelScope.launch { + downloadJob = viewModelScope.launch { ... - } finally { + } finally { + downloadInProgress.set(false) if (!downloadComplete && tmpFile.exists()) { tmpFile.delete() } } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/lora/LoraViewModel.kt` around lines 185 - 196, downloadAdapter currently checks _uiState.value.downloadingAdapterId non-atomically which can allow concurrent downloads; make the start sequence atomic by adding a guard (e.g., an AtomicBoolean or a Coroutine Mutex) that is checked-and-set before updating _uiState and launching the coroutine, then clear the guard when the download finishes or fails; update references in downloadAdapter, downloadJob, and any cleanup logic in viewModelScope to use this guard so only one download can be started concurrently.
🧹 Nitpick comments (2)
examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/lora/LoraViewModel.kt (2)
45-45: Prefix Android-specific class nameThis lives under an Android module; rename to something like
AndroidLoraViewModel(and file name) to comply with the Android-prefix naming guideline.As per coding guidelines: "Name Android-specific implementations with clear 'Android' prefix (e.g., 'AndroidTTSService.kt' not 'TTSService.kt' or 'TTSServiceImpl.kt')".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/lora/LoraViewModel.kt` at line 45, Rename the Android-specific ViewModel class LoraViewModel to AndroidLoraViewModel (and rename the Kotlin file accordingly) to follow the Android-prefix guideline; update the class declaration (class AndroidLoraViewModel(application: Application) : AndroidViewModel(application)), then update all references/imports/usages (ViewModelProvider, factories, DI bindings, XML bindings, unit tests) to the new symbol AndroidLoraViewModel so the project compiles with the new name. Ensure constructor signature and package remain unchanged aside from the name change.
150-158: Prefer a suspend API over callbackThis is already coroutine-based; exposing a
suspendfunction simplifies usage and aligns with the coroutine-first guideline. Update callers to invoke from a coroutine scope.♻️ Proposed change
- fun checkCompatibility(loraPath: String, onResult: (LoraCompatibilityResult) -> Unit) { - viewModelScope.launch { - val result = withContext(Dispatchers.IO) { - RunAnywhere.checkLoraCompatibility(loraPath) - } - onResult(result) - } - } + suspend fun checkCompatibility(loraPath: String): LoraCompatibilityResult = + withContext(Dispatchers.IO) { + RunAnywhere.checkLoraCompatibility(loraPath) + }Based on learnings: "Use suspend functions and coroutines for async operations instead of async/await patterns".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/lora/LoraViewModel.kt` around lines 150 - 158, Replace the callback-based checkCompatibility with a suspend API: change the function checkCompatibility(loraPath: String, onResult: (LoraCompatibilityResult) -> Unit) to a suspend function suspend fun checkCompatibility(loraPath: String): LoraCompatibilityResult that calls RunAnywhere.checkLoraCompatibility(loraPath) on the IO dispatcher and returns the result; remove viewModelScope.launch and the onResult lambda, and update all callers to call the new suspend function from a coroutine scope (e.g., viewModelScope.launch) and handle the returned LoraCompatibilityResult.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/lora/LoraViewModel.kt`:
- Around line 201-231: The download currently marks success without validating
bytes or integrity; after the IO loop in the coroutine (the block using
entry.downloadUrl, tmpFile, destFile, downloaded and totalSize) add a
verification step that compares downloaded to totalSize and fails (delete
tmpFile and throw) if they differ, and if the catalog supplies a checksum for
the entry use it to validate tmpFile before renaming (fail+cleanup on mismatch);
only call tmpFile.renameTo(destFile) and set downloadComplete = true after these
checks and ensure _uiState.update reflects any failure state.
- Around line 45-53: LoraViewModel currently contains LoRA business logic
(downloadJob, loraDir, and state handling) that should live in commonMain;
extract a shared LoRA controller/service in commonMain (e.g., LoraController or
LoraRepository) exposing functions and state flows for adapter lifecycle
(startDownload/stopDownload, getAdapters, adapterDir) and move any file/io or
coroutine logic there, then have LoraViewModel only hold a reference to that
shared interface and forward UI interactions to it while mapping its StateFlow
to _uiState; update uses of downloadJob, loraDir, and direct file operations in
LoraViewModel to delegate to the new common class and keep the AndroidViewModel
thin.
- Around line 31-38: The UI state uses a stringly-typed error field; replace
error: String? in LoraUiState with a sealed class (e.g., sealed class LoraError
{ object Network : LoraError(); data class DownloadFailed(val cause: Throwable):
LoraError(); object None : LoraError() } or similar) and update all
producers/consumers to use that type; modify LoraViewModel methods that set
error (catch blocks and state updates) to map exceptions to concrete LoraError
cases and set the new typed field, and update any UI consumers to handle
LoraError variants instead of raw strings (refer to LoraUiState, LoraViewModel
catch blocks and any callers that read error).
---
Duplicate comments:
In
`@examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/lora/LoraViewModel.kt`:
- Around line 176-182: scanDownloadedAdapters currently constructs File(loraDir,
entry.filename) directly and can be exploited via path traversal (e.g., "../") —
canonicalize and validate resolved paths before using them: in
scanDownloadedAdapters (and similarly in adapter download and delete functions),
resolve the candidate file's canonical/absolute path and ensure it startsWith
the canonical path of loraDir; if validation fails, skip or reject the entry and
do not access the file. Also normalize input (reject empty or absolute
filenames) and log or surface a clear error for invalid filenames.
- Around line 185-196: downloadAdapter currently checks
_uiState.value.downloadingAdapterId non-atomically which can allow concurrent
downloads; make the start sequence atomic by adding a guard (e.g., an
AtomicBoolean or a Coroutine Mutex) that is checked-and-set before updating
_uiState and launching the coroutine, then clear the guard when the download
finishes or fails; update references in downloadAdapter, downloadJob, and any
cleanup logic in viewModelScope to use this guard so only one download can be
started concurrently.
In `@sdk/runanywhere-commons/include/rac/core/rac_core.h`:
- Around line 343-350: The doc for rac_get_lora_registry is ambiguous about NULL
returns; update the comment for rac_get_lora_registry to match
rac_get_model_registry's clarity by either (A) explicitly stating "The registry
is created automatically on first access and this function never returns NULL;
on allocation failure the process aborts" if the implementation aborts on OOM,
or (B) explicitly stating "Callers must check the return value for NULL; NULL
indicates registry initialization failure (e.g., OOM) and should be handled as
an error" if the implementation can return NULL; reference rac_get_lora_registry
and rac_get_model_registry when making the change so the contract is consistent
and documents lifecycle and error handling expectations.
In
`@sdk/runanywhere-commons/src/infrastructure/model_management/lora_registry.cpp`:
- Around line 102-114: In rac_lora_registry_register, avoid freeing the existing
entry before you successfully create a deep copy: call
deep_copy_lora_entry(entry) first and check for nullptr (handle OOM) and only
after a successful copy call free_lora_entry on the old pointer and update
handle->entries[adapter_id] = copy; ensure you still hold the lock
(std::lock_guard<std::mutex> lock(handle->mutex)) while checking/updating the
map and keep the RAC_LOG_DEBUG and return paths unchanged.
- Around line 24-64: The function deep_copy_lora_entry calls free_lora_entry
before its definition, causing a compile error; add a forward declaration for
free_lora_entry (matching its static signature) immediately above
deep_copy_lora_entry so the compiler knows the symbol prior to use, then rebuild
to confirm the error is resolved (refer to deep_copy_lora_entry and
free_lora_entry).
- Around line 19-21: The registry uses raw std::string keys (entries in struct
rac_lora_registry) which violates the "use structured types" rule; introduce a
typed ID (e.g., a small wrapper type or a using/typedef such as LoraId) and
replace the map key type in rac_lora_registry::entries from std::string to that
new LoraId type, then update all API signatures and uses that take or compare
adapter/model IDs (including functions that construct, lookup, insert, or erase
rac_lora_entry_t entries) to accept and use LoraId instead of std::string so the
project consistently uses the structured ID type.
- Around line 84-90: The function rac_lora_registry_create uses plain new which
can throw std::bad_alloc across the C boundary; change allocation to use new
(std::nothrow) when constructing rac_lora_registry, check the returned pointer
for nullptr, and if null return RAC_ERROR_OUT_OF_MEMORY (and optionally log an
error) instead of allowing an exception to propagate; update the code paths that
assign *out_handle and return RAC_SUCCESS only when the allocation succeeded
(symbol: rac_lora_registry_create and type rac_lora_registry).
---
Nitpick comments:
In
`@examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/lora/LoraViewModel.kt`:
- Line 45: Rename the Android-specific ViewModel class LoraViewModel to
AndroidLoraViewModel (and rename the Kotlin file accordingly) to follow the
Android-prefix guideline; update the class declaration (class
AndroidLoraViewModel(application: Application) : AndroidViewModel(application)),
then update all references/imports/usages (ViewModelProvider, factories, DI
bindings, XML bindings, unit tests) to the new symbol AndroidLoraViewModel so
the project compiles with the new name. Ensure constructor signature and package
remain unchanged aside from the name change.
- Around line 150-158: Replace the callback-based checkCompatibility with a
suspend API: change the function checkCompatibility(loraPath: String, onResult:
(LoraCompatibilityResult) -> Unit) to a suspend function suspend fun
checkCompatibility(loraPath: String): LoraCompatibilityResult that calls
RunAnywhere.checkLoraCompatibility(loraPath) on the IO dispatcher and returns
the result; remove viewModelScope.launch and the onResult lambda, and update all
callers to call the new suspend function from a coroutine scope (e.g.,
viewModelScope.launch) and handle the returned LoraCompatibilityResult.
ℹ️ Review info
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (15)
Package.swiftexamples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/lora/LoraAdapterPickerSheet.ktexamples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/lora/LoraViewModel.ktexamples/ios/RunAnywhereAI/Package.resolvedexamples/ios/RunAnywhereAI/RunAnywhereAI.xcodeproj/project.pbxprojexamples/ios/RunAnywhereAI/RunAnywhereAI/App/RunAnywhereAIApp.swiftsdk/runanywhere-commons/CMakeLists.txtsdk/runanywhere-commons/include/rac/core/rac_core.hsdk/runanywhere-commons/include/rac/infrastructure/model_management/rac_model_types.hsdk/runanywhere-commons/scripts/build-ios.shsdk/runanywhere-commons/src/infrastructure/model_management/lora_registry.cppsdk/runanywhere-commons/src/infrastructure/model_management/model_registry.cppsdk/runanywhere-swift/Sources/RunAnywhere/CRACommons/include/CRACommons.hsdk/runanywhere-swift/Sources/RunAnywhere/CRACommons/include/rac_model_types.hvoic-ai-poc
✅ Files skipped from review due to trivial changes (1)
- voic-ai-poc
🚧 Files skipped from review as they are similar to previous changes (8)
- examples/ios/RunAnywhereAI/RunAnywhereAI/App/RunAnywhereAIApp.swift
- examples/ios/RunAnywhereAI/Package.resolved
- sdk/runanywhere-swift/Sources/RunAnywhere/CRACommons/include/CRACommons.h
- examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/lora/LoraAdapterPickerSheet.kt
- Package.swift
- sdk/runanywhere-commons/CMakeLists.txt
- sdk/runanywhere-swift/Sources/RunAnywhere/CRACommons/include/rac_model_types.h
- examples/ios/RunAnywhereAI/RunAnywhereAI.xcodeproj/project.pbxproj
| data class LoraUiState( | ||
| val registeredAdapters: List<LoraAdapterCatalogEntry> = emptyList(), | ||
| val loadedAdapters: List<LoRAAdapterInfo> = emptyList(), | ||
| val compatibleAdapters: List<LoraAdapterCatalogEntry> = emptyList(), | ||
| val downloadedAdapterPaths: Map<String, String> = emptyMap(), | ||
| val downloadingAdapterId: String? = null, | ||
| val downloadProgress: Float = 0f, | ||
| val error: String? = null, |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
🧩 Analysis chain
🏁 Script executed:
# First, let's read the LoraViewModel.kt file to see the actual code
cat -n examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/lora/LoraViewModel.kt | head -100Repository: RunanywhereAI/runanywhere-sdks
Length of output: 4907
🏁 Script executed:
# Search for how errors are currently handled in this file and if there are catch blocks
rg "catch|error|Error|sealed" examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/lora/LoraViewModel.kt -A 2 -B 1Repository: RunanywhereAI/runanywhere-sdks
Length of output: 3382
🏁 Script executed:
# Search for sealed error types defined in the codebase
rg "sealed class.*Error|sealed interface.*Error" --type ktRepository: RunanywhereAI/runanywhere-sdks
Length of output: 100
🏁 Script executed:
# Look for error handling patterns in the Android presentation layer
fd "ViewModel\.kt$" examples/android --exec rg -l "error.*String\|sealed.*Error" {} \;Repository: RunanywhereAI/runanywhere-sdks
Length of output: 56
🏁 Script executed:
# Check if there are any error wrapper types or Result types used in the codebase
rg "sealed class.*Result|data class.*Error|sealed interface.*Error" --type kt | head -20Repository: RunanywhereAI/runanywhere-sdks
Length of output: 100
Replace stringly-typed error with a sealed error type
error: String? loses type safety and context about failure modes. Introduce a sealed LoraError type (or similar) to represent specific error cases, then propagate this through LoraUiState and all catch blocks for type-safe, consistent error handling.
Per coding guidelines: "Use sealed classes for type-safe error handling rather than generic exception handling" and "Always use structured types and data classes instead of strings directly for type safety and consistency".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/lora/LoraViewModel.kt`
around lines 31 - 38, The UI state uses a stringly-typed error field; replace
error: String? in LoraUiState with a sealed class (e.g., sealed class LoraError
{ object Network : LoraError(); data class DownloadFailed(val cause: Throwable):
LoraError(); object None : LoraError() } or similar) and update all
producers/consumers to use that type; modify LoraViewModel methods that set
error (catch blocks and state updates) to map exceptions to concrete LoraError
cases and set the new typed field, and update any UI consumers to handle
LoraError variants instead of raw strings (refer to LoraUiState, LoraViewModel
catch blocks and any callers that read error).
| class LoraViewModel(application: Application) : AndroidViewModel(application) { | ||
|
|
||
| private val _uiState = MutableStateFlow(LoraUiState()) | ||
| val uiState: StateFlow<LoraUiState> = _uiState.asStateFlow() | ||
| private var downloadJob: Job? = null | ||
|
|
||
| private val loraDir: File by lazy { | ||
| File(application.filesDir, "lora_adapters").also { it.mkdirs() } | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Move LoRA business logic into commonMain; keep AndroidViewModel thin
Most adapter lifecycle logic here is business logic. Extract it into commonMain (e.g., a shared controller/service) and have this AndroidViewModel delegate UI binding only.
As per coding guidelines: "Define all business logic, protocols, interfaces, and structures in commonMain/ rather than in platform-specific modules".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/lora/LoraViewModel.kt`
around lines 45 - 53, LoraViewModel currently contains LoRA business logic
(downloadJob, loraDir, and state handling) that should live in commonMain;
extract a shared LoRA controller/service in commonMain (e.g., LoraController or
LoraRepository) exposing functions and state flows for adapter lifecycle
(startDownload/stopDownload, getAdapters, adapterDir) and move any file/io or
coroutine logic there, then have LoraViewModel only hold a reference to that
shared interface and forward UI interactions to it while mapping its StateFlow
to _uiState; update uses of downloadJob, loraDir, and direct file operations in
LoraViewModel to delegate to the new common class and keep the AndroidViewModel
thin.
| withContext(Dispatchers.IO) { | ||
| val connection = URL(entry.downloadUrl).openConnection().apply { | ||
| connectTimeout = 30_000 | ||
| readTimeout = 60_000 | ||
| } | ||
| connection.connect() | ||
| val totalSize = connection.contentLengthLong.takeIf { it > 0 } ?: entry.fileSize | ||
| var downloaded = 0L | ||
|
|
||
| connection.getInputStream().buffered().use { input -> | ||
| tmpFile.outputStream().buffered().use { output -> | ||
| val buffer = ByteArray(8192) | ||
| var bytesRead: Int | ||
| while (input.read(buffer).also { bytesRead = it } != -1) { | ||
| ensureActive() | ||
| output.write(buffer, 0, bytesRead) | ||
| downloaded += bytesRead | ||
| if (totalSize > 0) { | ||
| val progress = (downloaded.toFloat() / totalSize).coerceIn(0f, 1f) | ||
| _uiState.update { it.copy(downloadProgress = progress) } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| destFile.delete() | ||
| if (!tmpFile.renameTo(destFile)) { | ||
| tmpFile.delete() | ||
| throw Exception("Failed to move downloaded file to final location") | ||
| } | ||
| downloadComplete = true | ||
| } |
There was a problem hiding this comment.
Verify downloaded size (and checksum) before success
The download marks success without validating bytes received. At minimum, fail if downloaded doesn’t match the expected size; ideally also validate a checksum from the catalog to prevent tampering/partial downloads.
🛡️ Proposed size check
while (input.read(buffer).also { bytesRead = it } != -1) {
ensureActive()
output.write(buffer, 0, bytesRead)
downloaded += bytesRead
if (totalSize > 0) {
val progress = (downloaded.toFloat() / totalSize).coerceIn(0f, 1f)
_uiState.update { it.copy(downloadProgress = progress) }
}
}
}
}
+ if (totalSize > 0 && downloaded != totalSize) {
+ throw Exception("Incomplete download: expected $totalSize bytes, got $downloaded")
+ }
destFile.delete()
if (!tmpFile.renameTo(destFile)) {
tmpFile.delete()
throw Exception("Failed to move downloaded file to final location")
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/lora/LoraViewModel.kt`
around lines 201 - 231, The download currently marks success without validating
bytes or integrity; after the IO loop in the coroutine (the block using
entry.downloadUrl, tmpFile, destFile, downloaded and totalSize) add a
verification step that compares downloaded to totalSize and fails (delete
tmpFile and throw) if they differ, and if the catalog supplies a checksum for
the entry use it to validate tmpFile before renaming (fail+cleanup on mismatch);
only call tmpFile.renameTo(destFile) and set downloadComplete = true after these
checks and ensure _uiState.update reflects any failure state.
There was a problem hiding this comment.
♻️ Duplicate comments (4)
sdk/runanywhere-commons/src/infrastructure/model_management/lora_registry.cpp (4)
104-116:⚠️ Potential issue | 🔴 CriticalDangling pointer left in map when re-registration fails with OOM.
The old entry is freed at Line 110 before the deep copy at Line 111 succeeds. If
deep_copy_lora_entryreturnsnullptr,handle->entries[adapter_id]retains a pointer to already-freed memory, causing UB on any subsequentget,get_all,get_for_model, orremovecall.🐛 Proposed fix — deep-copy first, free old entry only on success
rac_result_t rac_lora_registry_register(rac_lora_registry_handle_t handle, const rac_lora_entry_t* entry) { if (!handle || !entry || !entry->id) return RAC_ERROR_INVALID_ARGUMENT; std::lock_guard<std::mutex> lock(handle->mutex); std::string adapter_id = entry->id; + rac_lora_entry_t* copy = deep_copy_lora_entry(entry); + if (!copy) return RAC_ERROR_OUT_OF_MEMORY; auto it = handle->entries.find(adapter_id); - if (it != handle->entries.end()) { free_lora_entry(it->second); } - rac_lora_entry_t* copy = deep_copy_lora_entry(entry); - if (!copy) return RAC_ERROR_OUT_OF_MEMORY; + if (it != handle->entries.end()) { free_lora_entry(it->second); } handle->entries[adapter_id] = copy; RAC_LOG_DEBUG("LoraRegistry", "LoRA adapter registered: %s", entry->id); return RAC_SUCCESS; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-commons/src/infrastructure/model_management/lora_registry.cpp` around lines 104 - 116, In rac_lora_registry_register, avoid freeing the existing entry before confirming deep_copy_lora_entry succeeds: first call deep_copy_lora_entry(entry) and if it returns nullptr return RAC_ERROR_OUT_OF_MEMORY without touching handle->entries; only after a successful deep copy, swap/assign the new pointer into handle->entries[adapter_id] and then free the previous pointer (using free_lora_entry) if one existed, all while holding the existing mutex so the map never contains a dangling pointer.
19-22:⚠️ Potential issue | 🟠 MajorRaw
std::stringmap key — structured type required.
std::map<std::string, rac_lora_entry_t*>still uses a plain string as the key. The same violation persists at every site that constructs astd::string adapter_idfrom rawconst char*(Lines 108, 121, 187).As per coding guidelines, "Always use structured types, never use strings directly for consistency and scalability." Introduce a typed ID wrapper (e.g.,
struct RacLoraAdapterId) and replace allstd::stringkey/parameter usages with it.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-commons/src/infrastructure/model_management/lora_registry.cpp` around lines 19 - 22, The registry currently uses raw std::string keys (rac_lora_registry::entries) and constructs std::string adapter_id from const char* at several sites; define a typed ID wrapper (e.g., struct RacLoraAdapterId) that holds the underlying std::string, provides a constructor from const char*/std::string, an operator< for use in std::map (and operator== or std::hash if needed elsewhere), and an accessor like str() or value(); then change rac_lora_registry::entries to std::map<RacLoraAdapterId, rac_lora_entry_t*> and replace all function parameters/locals that currently use std::string adapter_id (the places constructing from const char*) to use RacLoraAdapterId instead, updating comparisons and map lookups to use the new type.
86-92:⚠️ Potential issue | 🔴 Critical
new rac_lora_registry()can throwstd::bad_allocacross theextern "C"boundary.This was flagged previously and remains unaddressed. An uncaught C++ exception in an
extern "C"function results instd::terminate.🐛 Proposed fix
rac_result_t rac_lora_registry_create(rac_lora_registry_handle_t* out_handle) { if (!out_handle) return RAC_ERROR_INVALID_ARGUMENT; - rac_lora_registry* registry = new rac_lora_registry(); + rac_lora_registry* registry = new (std::nothrow) rac_lora_registry(); + if (!registry) return RAC_ERROR_OUT_OF_MEMORY; RAC_LOG_INFO("LoraRegistry", "LoRA registry created"); *out_handle = registry; return RAC_SUCCESS; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-commons/src/infrastructure/model_management/lora_registry.cpp` around lines 86 - 92, The function rac_lora_registry_create allocates with new rac_lora_registry() which can throw std::bad_alloc across the extern "C" boundary; wrap the allocation in a try/catch block (catch std::bad_alloc and a catch-all) so no C++ exceptions escape, only set *out_handle and return RAC_SUCCESS on success, and on failure log an error via RAC_LOG_ERROR and return an appropriate error code (e.g., RAC_ERROR_OUT_OF_MEMORY or RAC_ERROR_INTERNAL); reference rac_lora_registry_create, rac_lora_registry, *out_handle, and RAC_LOG_* to locate and update the code.
9-14:⚠️ Potential issue | 🟡 MinorFix allocation safety: use
new (std::nothrow)and prevent dangling pointer on OOM during re-registration.Line 88 allocates without
(std::nothrow), which will throw across the C boundary on failure. The codebase pattern (diffusion_component.cpp, backends/onnx, etc.) usesnew (std::nothrow)universally. Change tonew (std::nothrow)and handle null.Lines 110–113 create a dangling pointer: the old entry is freed before
deep_copy_lora_entry()succeeds. If deep_copy fails with OOM, the old entry has already been freed but the registry lookup still references it. Defer the free until after the copy succeeds:auto it = handle->entries.find(adapter_id); rac_lora_entry_t* copy = deep_copy_lora_entry(entry); if (!copy) return RAC_ERROR_OUT_OF_MEMORY; if (it != handle->entries.end()) free_lora_entry(it->second); handle->entries[adapter_id] = copy;Additionally, line 20 uses
std::map<std::string, ...>with a raw string key, violating the coding guideline to use structured types instead of strings directly.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-commons/src/infrastructure/model_management/lora_registry.cpp` around lines 9 - 14, Change raw allocations to use new (std::nothrow) and check for null returns: replace any plain new of rac_lora_entry_t (or other heap allocations in this file) with new (std::nothrow) and return RAC_ERROR_OUT_OF_MEMORY (or appropriate error) if the result is null. Fix the re-registration flow around handle->entries and deep_copy_lora_entry: obtain the iterator (auto it = handle->entries.find(adapter_id)); create the copy first (rac_lora_entry_t* copy = deep_copy_lora_entry(entry)); if copy is null return RAC_ERROR_OUT_OF_MEMORY; only then free the old entry via free_lora_entry(it->second) and assign handle->entries[adapter_id] = copy. Finally, replace the std::map<std::string, ...> key type with a structured adapter identifier type (use the existing adapter_id type/struct used elsewhere) instead of raw std::string to satisfy the coding guideline.
🧹 Nitpick comments (1)
sdk/runanywhere-commons/src/infrastructure/model_management/lora_registry.cpp (1)
94-100: Log message emitted afterdelete handle.
RAC_LOG_DEBUGat Line 99 is called afterdelete handle. While there is no UB here since the log doesn't dereferencehandle, the log conceptually describes the destruction that has already occurred and would appear out of order in traces. Move the log beforedelete.♻️ Proposed fix
void rac_lora_registry_destroy(rac_lora_registry_handle_t handle) { if (!handle) return; for (auto& pair : handle->entries) { free_lora_entry(pair.second); } handle->entries.clear(); + RAC_LOG_DEBUG("LoraRegistry", "LoRA registry destroyed"); delete handle; - RAC_LOG_DEBUG("LoraRegistry", "LoRA registry destroyed"); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-commons/src/infrastructure/model_management/lora_registry.cpp` around lines 94 - 100, The debug log in rac_lora_registry_destroy is emitted after delete handle which makes the trace show destruction after the object is gone; move the RAC_LOG_DEBUG("LoraRegistry", "LoRA registry destroyed") call to just before delete handle (after clearing entries and freeing each entry via free_lora_entry) so the log reflects the impending destruction while handle is still valid; update rac_lora_registry_destroy to log before the delete and keep the entries.clear() and free_lora_entry(pair.second) calls intact.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In
`@sdk/runanywhere-commons/src/infrastructure/model_management/lora_registry.cpp`:
- Around line 104-116: In rac_lora_registry_register, avoid freeing the existing
entry before confirming deep_copy_lora_entry succeeds: first call
deep_copy_lora_entry(entry) and if it returns nullptr return
RAC_ERROR_OUT_OF_MEMORY without touching handle->entries; only after a
successful deep copy, swap/assign the new pointer into
handle->entries[adapter_id] and then free the previous pointer (using
free_lora_entry) if one existed, all while holding the existing mutex so the map
never contains a dangling pointer.
- Around line 19-22: The registry currently uses raw std::string keys
(rac_lora_registry::entries) and constructs std::string adapter_id from const
char* at several sites; define a typed ID wrapper (e.g., struct
RacLoraAdapterId) that holds the underlying std::string, provides a constructor
from const char*/std::string, an operator< for use in std::map (and operator==
or std::hash if needed elsewhere), and an accessor like str() or value(); then
change rac_lora_registry::entries to std::map<RacLoraAdapterId,
rac_lora_entry_t*> and replace all function parameters/locals that currently use
std::string adapter_id (the places constructing from const char*) to use
RacLoraAdapterId instead, updating comparisons and map lookups to use the new
type.
- Around line 86-92: The function rac_lora_registry_create allocates with new
rac_lora_registry() which can throw std::bad_alloc across the extern "C"
boundary; wrap the allocation in a try/catch block (catch std::bad_alloc and a
catch-all) so no C++ exceptions escape, only set *out_handle and return
RAC_SUCCESS on success, and on failure log an error via RAC_LOG_ERROR and return
an appropriate error code (e.g., RAC_ERROR_OUT_OF_MEMORY or RAC_ERROR_INTERNAL);
reference rac_lora_registry_create, rac_lora_registry, *out_handle, and
RAC_LOG_* to locate and update the code.
- Around line 9-14: Change raw allocations to use new (std::nothrow) and check
for null returns: replace any plain new of rac_lora_entry_t (or other heap
allocations in this file) with new (std::nothrow) and return
RAC_ERROR_OUT_OF_MEMORY (or appropriate error) if the result is null. Fix the
re-registration flow around handle->entries and deep_copy_lora_entry: obtain the
iterator (auto it = handle->entries.find(adapter_id)); create the copy first
(rac_lora_entry_t* copy = deep_copy_lora_entry(entry)); if copy is null return
RAC_ERROR_OUT_OF_MEMORY; only then free the old entry via
free_lora_entry(it->second) and assign handle->entries[adapter_id] = copy.
Finally, replace the std::map<std::string, ...> key type with a structured
adapter identifier type (use the existing adapter_id type/struct used elsewhere)
instead of raw std::string to satisfy the coding guideline.
---
Nitpick comments:
In
`@sdk/runanywhere-commons/src/infrastructure/model_management/lora_registry.cpp`:
- Around line 94-100: The debug log in rac_lora_registry_destroy is emitted
after delete handle which makes the trace show destruction after the object is
gone; move the RAC_LOG_DEBUG("LoraRegistry", "LoRA registry destroyed") call to
just before delete handle (after clearing entries and freeing each entry via
free_lora_entry) so the log reflects the impending destruction while handle is
still valid; update rac_lora_registry_destroy to log before the delete and keep
the entries.clear() and free_lora_entry(pair.second) calls intact.
ℹ️ Review info
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
sdk/runanywhere-commons/src/infrastructure/model_management/lora_registry.cppsdk/runanywhere-swift/Sources/RunAnywhere/CRACommons/shim.c
- C++: use new(std::nothrow) to prevent std::bad_alloc across C boundary - C++: fix dangling pointer on OOM during lora re-registration - C++: add empty-path checks to load_lora and remove_lora - Kotlin SDK: replace fragile regex JSON parsing with org.json - Android: add HTTPS-only URL validation for adapter downloads - Android: validate downloaded file size against catalog entry - Android: fix concurrent refreshLoraState race condition - Android: move LoraViewModel out of composable body in ChatScreen - Android: wrap Timber.i in LaunchedEffect in MainActivity - Docs: add per-function error codes to rac_lora_registry.h Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Summary
Redesigned the chat screen to a cleaner ChatGPT-like layout and added full LoRA adapter management UI to the Android demo app.
Chat Redesign
LoRA UI
Changes
examples/android/RunAnywhereAI/): Chat redesign, LoRA picker/manager screens, updated navigationui/theme/): New spacing constants, solid bubble colors, LoRA badge colorsdata/ModelList.kt): Real LoRA adapter catalog entriesTest plan
Summary by CodeRabbit
New Features
New Features (UI)
New Features (Build)
Chores
Greptile Summary
This PR delivers a comprehensive chat redesign with full LoRA adapter support. The chat UI now features a clean ChatGPT-style layout with simplified bubbles, a model chip TopBar, and inline LoRA badges. The LoRA implementation includes adapter catalog registration, download management with progress tracking, runtime loading/unloading with adjustable scale, and dedicated UI screens (picker bottom sheet + full manager).
Key changes:
Void2377/Qwen) marked compatible with LFM2-350M modelsRunAnyWhereLoraexample app (consolidated into main demo)Critical security issues found:
LoraViewModel.kt)Confidence Score: 2/5
LoraViewModel.kt(security vulnerabilities in download),ModelList.kt(verify LoRA compatibility claims)Important Files Changed
LoraViewModelinstance created on every recomposition (line 80) causes state loss.Flowchart
%%{init: {'theme': 'neutral'}}%% flowchart TD Start[User Opens Chat Screen] --> CheckModel{Model Loaded?} CheckModel -->|Yes| ShowTopBar[Show Model Chip TopBar] CheckModel -->|No| ShowOverlay[Show Model Selection Overlay] ShowTopBar --> CheckLoRA{Model Supports LoRA?} CheckLoRA -->|Yes| ShowLoRAButton[Display + LoRA Button] CheckLoRA -->|No| HideLoRAButton[Hide LoRA Controls] ShowLoRAButton --> UserClicksLoRA[User Clicks + LoRA] UserClicksLoRA --> OpenPicker[Open LoraAdapterPickerSheet] OpenPicker --> FetchCompatible[LoraViewModel.refreshForModel] FetchCompatible --> FilterAdapters[Filter by compatibleModelIds] FilterAdapters --> ShowAdapters[Display Compatible Adapters] ShowAdapters --> CheckDownloaded{Adapter Downloaded?} CheckDownloaded -->|No| DownloadFlow[Download Button] CheckDownloaded -->|Yes| ApplyFlow[Scale Slider + Apply Button] DownloadFlow --> StartDownload[downloadAdapter] StartDownload --> HTTPRequest[URL.openConnection] HTTPRequest --> ProgressUpdate[Update downloadProgress] ProgressUpdate --> SaveFile[Save to lora_adapters/] SaveFile --> ShowApply[Show Apply UI] ApplyFlow --> SetScale[User Adjusts Scale 0-2] SetScale --> ClickApply[Click Apply Button] ClickApply --> LoadAdapter[loadAdapter path scale] LoadAdapter --> SDKBridge[RunAnywhere.loadLoraAdapter] SDKBridge --> UpdateState[Update loadedAdapters List] UpdateState --> ShowBadge[Display LoRA Badge on TopBar] ShowBadge --> ChatInference[User Sends Message] ChatInference --> ApplyLoRA[LoRA Applied During Inference]Last reviewed commit: b83a6ac