Skip to content

[Android App] ChatGPT-style chat redesign + LoRA adapter UI#414

Merged
sanchitmonga22 merged 16 commits intomainfrom
siddhesh/lora-demo
Feb 25, 2026
Merged

[Android App] ChatGPT-style chat redesign + LoRA adapter UI#414
sanchitmonga22 merged 16 commits intomainfrom
siddhesh/lora-demo

Conversation

@Siddhesh2377
Copy link
Copy Markdown
Collaborator

@Siddhesh2377 Siddhesh2377 commented Feb 23, 2026

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

  • Removed bubble styling from assistant messages — now clean left-aligned text with model icon
  • Simplified user message bubbles to solid background (no gradients/shadows)
  • Removed nested Scaffold from ChatScreen (AppNavigation already provides one)
  • Cleaned up spacing, typing indicator, and analytics footer alignment

LoRA UI

  • Added LoRA badge on compatible model cards in the model picker
  • New LoRA adapter picker bottom sheet (accessible from chat top bar when a LoRA-compatible model is loaded)
  • New LoRA Manager screen under More > Model Customization
  • LoraViewModel handles download, load/unload, compatibility checking
  • Registered 5 real standalone LoRA GGUF adapters from HuggingFace (Void2377/Qwen)
  • LFM2-350M (both Q4_K_M and Q8_0) marked as LoRA-compatible

Changes

  • Android App (examples/android/RunAnywhereAI/): Chat redesign, LoRA picker/manager screens, updated navigation
  • Theme (ui/theme/): New spacing constants, solid bubble colors, LoRA badge colors
  • Data (data/ModelList.kt): Real LoRA adapter catalog entries

Test plan

  • Build and deploy to device
  • Verify chat UI shows clean layout (no assistant bubbles, simple user bubbles)
  • Load LFM2-350M and verify LoRA button appears in top bar
  • Open LoRA picker and verify adapters listed
  • Download and apply a LoRA adapter
  • Navigate to More > LoRA Adapters manager screen
  • Verify LoRA badge shows on LFM2-350M in model selection sheet

Summary by CodeRabbit

  • New Features

    • LoRA support added: adapter catalog, registration, compatibility checks, and runtime load/unload with adjustable scale across SDKs.
  • New Features (UI)

    • LoRA picker & manager screens; chat and model-selection surfaces updated to show LoRA controls; app-wide top-bar/navigation refinements.
  • New Features (Build)

    • New build tasks and scripts to build native C++ components, copy native libs, and run full SDK build pipeline.
  • Chores

    • Removed example Android LoRA project and cleaned IDE run configs; logging migrated to Timber.

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:

  • Chat redesign removes assistant message bubbles, adds model chip as TopBar title with inline LoRA badge
  • LoRA UI: picker bottom sheet from chat TopBar, full manager under More > Model Customization
  • Registered 5 LoRA adapters from HuggingFace (Void2377/Qwen) marked compatible with LFM2-350M models
  • Removed standalone RunAnyWhereLora example app (consolidated into main demo)
  • SDK: exposed LoRA registry APIs across Kotlin/Swift bridges

Critical security issues found:

  • HTTP downloads allowed without HTTPS validation (line 202 in LoraViewModel.kt)
  • No file size validation after download completes (line 224)
  • LoRA adapter architecture mismatch: Qwen-trained adapters registered as compatible with LiquidAI LFM2 models

Confidence Score: 2/5

  • This PR has critical security vulnerabilities and architectural issues that must be resolved before merge
  • Score reflects three critical issues: (1) HTTP download vulnerability allows MITM attacks on model files, (2) missing file integrity validation could lead to loading corrupt adapters, and (3) likely LoRA architecture mismatch will produce incorrect outputs. While the UI implementation is solid and the chat redesign is clean, these security and correctness issues make the PR unsafe to merge as-is.
  • LoraViewModel.kt (security vulnerabilities in download), ModelList.kt (verify LoRA compatibility claims)

Important Files Changed

Filename Overview
examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/chat/ChatScreen.kt Redesigned ChatGPT-style UI with model chip TopBar and inline LoRA badge. New LoraViewModel instance created on every recomposition (line 80) causes state loss.
examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/lora/LoraViewModel.kt LoRA adapter download/load/unload manager. Critical security issue: downloads over HTTP allowed (line 202), no file size validation against catalog entry.
examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/data/ModelList.kt Registered 5 LoRA adapters from Void2377/Qwen repo, marked compatible with LFM2-350M models. Architecture mismatch likely: Qwen-trained LoRAs applied to LiquidAI LFM2.

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]
Loading

Last reviewed commit: b83a6ac

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.
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 23, 2026

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds 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

Cohort / File(s) Summary
IDE run configs & VCS
.idea/runConfigurations/*, .idea/vcs.xml
Added and renumbered multiple Gradle run configurations; removed several VCS directory mappings.
Gradle tasks & build scripts
build.gradle.kts, sdk/runanywhere-kotlin/scripts/*
Added Gradle tasks buildCpp, buildFullSdk, copyNativeLibs; new orchestrator script build-sdk.sh; ensured JNI libs are always copied in build-kotlin.sh.
Removed Android example
examples/android/RunAnyWhereLora/**
Entire RunAnyWhereLora example removed (IDE files, gradle wrapper/settings, app sources, resources, tests, wrapper scripts).
Android RunAnywhereAI — LoRA UI & navigation
examples/android/RunAnyWhereAI/.../presentation/*, AppNavigation.kt, MoreHubScreen.kt
Added LoRA UI surfaces (LoraManagerScreen, LoraAdapterPickerSheet), ChatTopBar/ChatScreen wiring for LoRA, navigation route LORA_MANAGER, and top-bar state utilities.
Android RunAnywhereAI — LoRA VM & state
.../presentation/lora/*, LoraViewModel.kt, ChatViewModel.kt
New LoraViewModel and LoraUiState; ChatViewModel updated with LoRA state and refreshLoraState(); UI actions for download/load/unload and compatibility checks implemented.
Model registry & model metadata (C/C++)
sdk/runanywhere-commons/*, sdk/runanywhere-swift/*
Added in-memory LoRA registry (headers + implementation), global registry APIs (rac_get_lora_registry, rac_register_lora, rac_get_lora_for_model), and added supports_lora flag to public model info types across C/C++/Swift.
LLM LoRA compatibility check
sdk/runanywhere-commons/src/features/llm/*, include/rac_llm_component.h
Added rac_llm_component_check_lora_compat C API and implementation for lightweight compatibility pre-checks.
JNI / Native bridge additions
sdk/runanywhere-commons/src/jni/runanywhere_commons_jni.cpp, RunAnywhereBridge.kt
Added JNI functions for LoRA registry ops and compatibility; JSON serialization for LoRA entries; extended racModelRegistrySave JNI signature to include supportsLora.
Kotlin SDK public APIs & JVM bridges
sdk/runanywhere-kotlin/src/.../RunAnywhere+LoRA*, CppBridgeLoraRegistry.kt, CppBridgeLLM.kt
Added public types LoraCompatibilityResult and LoraAdapterCatalogEntry; new RunAnywhere APIs: checkLoraCompatibility, registerLoraAdapter, loraAdaptersForModel, allRegisteredLoraAdapters; JVM/bridge implementations to native registry.
Swift bridge & iOS example
sdk/runanywhere-swift/..., examples/ios/RunAnywhereAI/**
Added Swift CppBridge LoraRegistry actor, public LoraAdapterCatalogEntry/LoraCompatibilityResult types, and iOS-side registration of adapters at startup.
Build / CMake / exports
sdk/runanywhere-commons/CMakeLists.txt, sdk/runanywhere-commons/exports/RACommons.exports
Added lora_registry.cpp to sources and exported new LoRA/LLM compatibility symbols in the exports file.
Android UI/theme & misc refactors
examples/android/RunAnyWhereAI/...
Widespread UI refactors: new top-bar state system, theme/dimensions/colors expanded, many composables gained onBack/top-bar params; replaced android.util.Log with Timber across many files.
Miscellaneous
examples/android/RunAnywhereAI/..., Package.swift, various scripts
ABI/APK split adjustments, iOS Swift package dependency updates, removal of Rag bundling in iOS build script, and other small build/script tweaks.

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
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Suggested labels

kotlin-sample, android-sample, kotlin-sdk

Suggested reviewers

  • shubhammalhotra28

Poem

🐇 I hopped through headers, JNI, and builds,
A registry stitched with tiny thrills,
Kotlin, Swift, and C++ sing,
Adapters dance—what joy they bring!
Hooray — LoRA blossoms on our hills 🎉

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 64.06% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main changes: ChatGPT-style chat redesign and LoRA adapter UI for Android app.
Description check ✅ Passed The description provides a structured breakdown of changes across sections (Summary, Chat Redesign, LoRA UI, Changes, Test plan) with clear explanations of modifications and expected behavior.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch siddhesh/lora-demo

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:windowSoftInputMode is an <activity> attribute and will be silently ignored on <application>.

The official Android Developers documentation lists android:windowSoftInputMode exclusively 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 MainActivity declaration:

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, adjustResize on the activity works with Compose insets APIs. Pair android:windowSoftInputMode="adjustResize" with WindowCompat.setDecorFitsSystemWindows(window, false) in MainActivity.onCreate and Modifier.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: loraBadgeBg is 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 @Composable theme-aware accessor. loraBadgeBg has neither.

At alpha = 0.10f over a dark surface like backgroundPrimaryDark (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 — loraBadgeBg is a 10 % opacity variant of the same base color. Consider whether this deserves its own name or could be expressed as badgePurple.copy(alpha = 0.5f) (half of badgePurple) 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_t struct fields are documented with inline comments, which is fine. Just noting that compatible_model_ids and compatible_model_count have an implicit contract: compatible_model_ids is a char** array of length compatible_model_count, where each element is a heap-allocated char*. This ownership model should be made explicit, especially for consumers implementing their own entry construction (vs. using rac_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: Empty url = "" used as sentinel for multi-file models.

VLM models that use multi-file registration set url = "" because only files is used. This works because setupModels() checks model.files.isNotEmpty() before routing, but an empty string is a fragile sentinel. Consider making url nullable in AppModel so 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 use model.url != null or model.files.isNotEmpty() to decide the registration path, and single-file model registrations can assert model.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: RuntimeException violates 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 RuntimeException here makes it difficult for callers to handle registration failures gracefully. Consider using a sealed result type or the SDK's existing SDKError hierarchy, consistent with how CppBridgeLLM handles 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 using kotlinx.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.json is already a project dependency. Using Json.decodeFromString with a @Serializable data class would be more robust and less code. That said, this mirrors the pattern in CppBridgeLLM.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: extractBoolean is subtly inconsistent with other bridge files.

This implementation uses RegexOption.IGNORE_CASE and .lowercase() == "true", whereas other bridge files (e.g., CppBridgeVoiceAgent.kt, CppBridgeVAD.kt) use .toBooleanStrictOrNull() ?: false without IGNORE_CASE. Since JSON strictly uses lowercase true/false, the IGNORE_CASE flag 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.

FLAGS is set via bash substring expansion on line 469 (${MODE:0:1}"l" or "r") but is immediately overwritten by the if/else on 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 omitting supportsLora from registerMultiFileModel is intentional.

Neither the List<ModelFileDescriptor> overload (lines 110–149) nor the companionFiles overload (lines 194–213) exposes a supportsLora parameter, so any model registered through these paths will always have supportsLora = 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: Move Log.i into a LaunchedEffect to avoid firing on every recomposition.

Calling Log.i directly in a composable body is an untracked side effect — it executes on every invocation of MainAppContent while the Ready state holds. Wrapping it in LaunchedEffect(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 the setup task.

If scripts/build-sdk.sh or scripts/build-kotlin.sh is absent (e.g., on a fresh checkout where the script hasn't been committed/pulled), Gradle will throw an opaque ExecException with no actionable message. The existing setup task guards this with a buildScript.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 buildFullSdk and copyNativeLibs.

🤖 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-line if/return vs 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 for LoraCompatibilityResult.

The coding guidelines recommend sealed classes for type-safe error handling. A sealed result would eliminate the possibility of isCompatible = true with a non-null error (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 -11 hard-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 a usage() 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.

Comment on lines +170 to +189
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)
}
}
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Suggested change
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.

Comment on lines +1277 to +1317
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());
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 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=h

Repository: 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.cpp

Repository: 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=h

Repository: 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.h

Repository: 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=kt

Repository: 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.h

Repository: 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.h

Repository: 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.

Comment on lines +1942 to +1959
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();
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ 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.

Comment on lines +44 to +55
# 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[@]}"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

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.

Suggested change
# 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.

- 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)
@Siddhesh2377
Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 23, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 established ICON SIZES section; assistantIconSpacing duplicates medium.

  • 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.dp is identical to medium. Using medium directly, 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: userBubbleCornerRadius is an alias of the already-existing messageBubbleCornerRadius.

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 reusing messageBubbleCornerRadius at 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: buildFullSdk is 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 sdk won'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 (like setup does) to avoid cryptic failures.

The setup task guards its exec with if (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_HOME is redundant in a copy-only task.

copyNativeLibs is described as "no C++ rebuild" and delegates to build-kotlin.sh --local --skip-build, which should not require the NDK. Setting ANDROID_NDK_HOME here 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, and log are all duplicated from CppBridgeModelRegistry.kt.

These utilities are nearly identical to the equivalents at CppBridgeModelRegistry.kt lines 385–440. Consider extracting them into a shared CppBridgeJsonUtils (or similar) object in jvmAndroidMain and 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: RuntimeException from CppBridgeLoraRegistry.register propagates uncaught through the public API.

CppBridgeLoraRegistry.register throws RuntimeException on failure. registerLoraAdapter lets it escape unchanged, making the public API inconsistent — all other LoRA functions (loadLoraAdapter, removeLoraAdapter, clearLoraAdapters) throw SDKError.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_t uses size_t (from <stddef.h>) and int64_t (from <stdint.h>). As a public C header consumed by external toolchains without the project's include-path configuration, relying on transitive inclusion from rac_types.h is 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: Prefer collectAsStateWithLifecycle() over collectAsState() for lifecycle safety.

ChatScreen already uses collectAsStateWithLifecycle for the same flow type. Using plain collectAsState() 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 using AppColors.primaryPurple instead of the hardcoded color.

Color(0xFF8B5CF6) is used here, while AppColors.primaryPurple is consistently used for LoRA-related UI elements throughout LoraAdapterPickerSheet and LoraManagerScreen. 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 the Column body.

Lines 104–124 (ChatTopBar) are at 12-space indent inside the Column, 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 three if (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 separate LoraViewModel instance scoped to this NavBackStackEntry.

ChatScreen holds its own LoraViewModel instance. A download started via ChatScreen'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 in LoraManagerScreen will never reflect an active download triggered from ChatScreen. 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.

Comment on lines +88 to +137
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,
),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 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
done

Repository: 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
done

Repository: 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 be 673664 (not 690_176)
  • summarizer-lora-Q8_0.gguf: should be 673632 (not 690_176)
  • translator-lora-Q8_0.gguf: should be 673632 (not 690_176)
  • sentiment-lora-Q8_0.gguf: should be 673632 (not 690_176)
  • uncensored_chat-lora-Q8_0.gguf: should be 1340288 (not 1_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.

Comment on lines +142 to +145
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),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 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 -20

Repository: 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:

  1. the LLM weights: SmolVLM-500M-Instruct-*.gguf and
  2. 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.

}
}
}
tmpFile.renameTo(destFile)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Suggested change
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.

Comment on lines 347 to 349
val supportsThinking: Boolean = false,
val supportsLora: Boolean = false,
val thinkingPattern: ThinkingTagPattern? = null,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

rg -n "supportsLora" --type swift

Repository: RunanywhereAI/runanywhere-sdks

Length of output: 56


🏁 Script executed:

#!/bin/bash
# Find Swift ModelTypes file
fd "ModelTypes" --type f --extension swift

Repository: 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.kt

Repository: 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.swift

Repository: 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.kt

Repository: 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.

Comment on lines +73 to +118
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()
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

JSON parsing is fragile: special characters inside string values corrupt extraction.

Two related issues:

  1. extractString (Line 93) — the [^"]* pattern cannot match JSON string values containing escaped quotes (e.g., "description": "Say \"hello\""). CppBridgeLLM.kt already uses the correct pattern (?:[^"\\]|\\.)* for this purpose.

  2. 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.

Suggested change
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.

@Siddhesh2377 Siddhesh2377 removed the request for review from shubhammalhotra28 February 23, 2026 02:32
@Siddhesh2377 Siddhesh2377 marked this pull request as draft February 23, 2026 02:32
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (5)
examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/MainActivity.kt (1)

61-81: Use when (val state = initState) to get safe smart casts and compiler-enforced exhaustiveness.

initState is a delegated property (by collectAsState()). Kotlin's smart-cast analysis does not apply to delegated properties because each read calls getValue on the delegate — so on line 67, initState is accessed again after the is SDKInitializationState.Error guard, and the explicit as cast 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 when is 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 new supportsLora parameter.

The @param list documents supportsThinking (Line 42) but omits supportsLora. Add a @param supportsLora entry 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-sensitive String.format may 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, use String.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 in rac_shutdown() — same as model registry.

Neither s_model_registry nor the new s_lora_registry are destroyed/freed during rac_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 for racModelRegistrySave is missing @param supportsLora

The 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!")
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Suggested change
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.

Comment on lines 646 to 651
AnalyticsFooter(
message = message,
hasThinking = message.thinkingContent != null,
alignEnd = false,
)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Comment on lines +803 to +813
/** 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)
}
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Comment on lines +27 to +40
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}")
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check how other CppBridge files handle errors
rg "throw RuntimeException|throw SDKError" --type=kt -g '*CppBridge*' -n -C1

Repository: 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 -30

Repository: 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 -10

Repository: 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 -20

Repository: 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 -10

Repository: 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 -40

Repository: RunanywhereAI/runanywhere-sdks

Length of output: 3567


🏁 Script executed:

#!/bin/bash
# Find SDKError definition
find . -name "*SDKError*" -type f | grep kotlin

Repository: 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 -50

Repository: 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 -100

Repository: 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 f

Repository: 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 2

Repository: 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 1

Repository: 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 -10

Repository: 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.

Suggested change
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.

Siddhesh2377 and others added 4 commits February 23, 2026 08:38
- 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>
@shubhammalhotra28
Copy link
Copy Markdown
Contributor

@coderabbitai please do a detailed review. Thanks!

@shubhammalhotra28
Copy link
Copy Markdown
Contributor

@greptile please do a detailed review. Thanks!

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 24, 2026

Tip

For best results, initiate chat on the files or code changes.

@shubhammalhotra28, I'll provide a comprehensive review of this PR. This is a substantial feature addition with changes across multiple layers of the codebase.

🏗️ Architecture & Design

Strengths

  1. Layered Architecture: Clean separation between C++ registry, JNI bridge, Kotlin multiplatform API, and UI layer
  2. Registry Pattern: LoRA registry mirrors the existing model registry pattern, maintaining consistency
  3. Catalog vs Runtime: Good separation between adapter catalog (registry) and runtime state (loaded adapters)

⚠️ Concerns

1. Memory Management in JNI Layer (runanywhere_commons_jni.cpp)

// Line ~800: racLoraRegistryRegister
jsize count = env->GetArrayLength(compatibleModelIds);
const char** model_ids = (const char**)malloc(sizeof(char*) * count);
// ... allocations ...
// If rac_register_lora fails, model_ids is leaked

Issue: Memory leak if registration fails. The cleanup only happens on success.

Recommendation: Use RAII or ensure cleanup in all paths:

struct ScopedJStringArray {
    JNIEnv* env;
    const char** strings;
    jsize count;
    ~ScopedJStringArray() {
        if (strings) {
            for (jsize i = 0; i < count; i++) {
                if (strings[i]) free((void*)strings[i]);
            }
            free(strings);
        }
    }
};

2. Thread Safety in LoraViewModel

// Line ~150: downloadAdapter
private var downloadJob: Job? = null

fun downloadAdapter(entry: LoraAdapterCatalogEntry) {
    if (downloadJob?.isActive == true) return
    downloadJob = viewModelScope.launch(Dispatchers.IO) {
        // ... download logic
    }
}

Issue: Race condition - two rapid calls could both pass the check before either sets downloadJob.

Recommendation: Use a mutex or atomic flag:

private val downloadMutex = Mutex()
suspend fun downloadAdapter(...) {
    downloadMutex.withLock {
        if (downloadJob?.isActive == true) return
        downloadJob = launch { ... }
    }
}

3. File Download Security (LoraViewModel.kt)

// No validation of downloaded file integrity
val outputFile = File(loraDir, entry.filename)

Issues:

  • No checksum validation
  • No size validation against expected fileSize
  • Potential path traversal if filename contains ".."

Recommendations:

  • Add SHA256 checksum to LoraAdapterCatalogEntry
  • Validate downloaded file size matches expected
  • Sanitize filename: entry.filename.replace("..", "")

🐛 Potential Bugs

4. Null Pointer in C++ Registry (lora_registry.cpp)

// Line ~120: get_for_model
if (model_id == nullptr) {
    return RAC_ERROR_INVALID_ARGUMENT;
}
// But compatible_model_ids array elements aren't null-checked
for (size_t j = 0; j < entry_ptr->compatible_model_ids_count; j++) {
    if (strcmp(entry_ptr->compatible_model_ids[j], model_id) == 0) {

Risk: If any compatible_model_ids[j] is null, strcmp crashes.

Fix: Add null check:

const char* compatible = entry_ptr->compatible_model_ids[j];
if (compatible && strcmp(compatible, model_id) == 0) {

5. UI State Race Condition (ChatViewModel.kt)

// Line ~85: refreshLoraState
fun refreshLoraState() {
    viewModelScope.launch(Dispatchers.IO) {
        try {
            val adapters = RunAnywhere.getLoadedLoraAdapters()
            _uiState.update { it.copy(hasActiveLoraAdapter = adapters.isNotEmpty()) }

Issue: If model is unloaded between call and state update, UI shows stale state.

Recommendation: Check model is still loaded before updating state, or handle model unload events.

📱 UI/UX Review

6. Chat UI Redesign (ChatScreen.kt)
The chat redesign is clean, but:

  • Inconsistency: User messages still have bubbles, assistant messages don't - this asymmetry may confuse users
  • Copy functionality: Removed context menu for copying messages - this was a useful feature
  • Accessibility: Ensure the new flat design has sufficient contrast for model icons

Recommendation: Consider adding a long-press menu for message actions (copy, share, etc.)

7. LoRA Scale Slider (LoraAdapterPickerSheet.kt)

Slider(
    value = scale,
    onValueChange = { scale = it },
    valueRange = 0f..2f,
    steps = 19
)

Good: Reasonable range (0-2) with decent granularity.
Suggestion: Show the current scale value as text near the slider for precision.

🔧 API Design

8. LoRA Compatibility Check API

data class LoraCompatibilityResult(
    val isCompatible: Boolean,
    val error: String? = null
)

Concern: Error as String loses structure. Consider:

sealed class LoraCompatibilityResult {
    object Compatible : LoraCompatibilityResult()
    data class Incompatible(val reason: CompatibilityError) : LoraCompatibilityResult()
}
enum class CompatibilityError {
    FILE_NOT_FOUND, MODEL_MISMATCH, INVALID_FORMAT, ...
}

9. Model Registration API Change

fun RunAnywhere.registerModel(
    // ... existing params
    supportsLora: Boolean = false
)

Good: Backward compatible with default false.
Question: Should this be auto-detected from model metadata instead of manually specified?

🚀 Performance

10. Synchronous Registry Access (CppBridgeLLM.kt)

fun checkLoraCompatibility(loraPath: String): String? = synchronized(lock) {
    // ... calls JNI
}

Issue: JNI call on caller's thread could block UI if called from main thread.

Recommendation: Make this suspending:

suspend fun checkLoraCompatibility(loraPath: String): String? = 
    withContext(Dispatchers.IO) {
        // ...
    }

11. JSON Parsing in JNI (CppBridgeLoraRegistry.kt)
Manual JSON parsing with string operations is fragile and slow:

private fun extractString(json: String, key: String): String? {
    val keyPattern = "\"$key\"\\s*:\\s*\"([^\"]*)\""
    // regex on every call
}

Recommendation: Use kotlinx.serialization or a proper JSON library.

🧹 Code Quality

12. Build Script Improvements (build-sdk.sh)

if [[ "$CPP_ONLY" == "true" && "$SKIP_CPP" == "true" ]]; then
    echo "Error: --skip-cpp and --cpp-only are mutually exclusive"
    exit 1
fi

Good: Validates mutually exclusive flags.
Minor: Consider using functions to reduce duplication in the exec calls.

13. Deleted RunAnyWhereLora App
The entire examples/android/RunAnyWhereLora directory was deleted. This is fine if it was a temporary/experimental app, but ensure:

  • No documentation references it
  • No CI/CD pipelines depend on it

🔒 Security

14. File Path Handling

// LoraViewModel.kt
fun localPath(entry: LoraAdapterCatalogEntry): String? {
    return File(loraDir, entry.filename).absolutePath
}

Risk: If entry.filename contains path traversal sequences ("../../../etc/passwd"), this could escape the intended directory.

Fix: Validate and sanitize:

fun localPath(entry: LoraAdapterCatalogEntry): String? {
    val sanitized = entry.filename.replace(Regex("[/\\\\]"), "_")
    val file = File(loraDir, sanitized)
    // Verify it's actually under loraDir
    if (!file.canonicalPath.startsWith(loraDir.canonicalPath)) {
        return null
    }
    return file.absolutePath
}

📝 Documentation & Testing

15. Missing Documentation

  • No KDoc for new public APIs (LoraAdapterCatalogEntry, checkLoraCompatibility, etc.)
  • No usage examples in code comments
  • Test plan mentions manual verification but no automated tests added

Recommendation: Add:

  • KDoc for all public APIs
  • Unit tests for LoRA registry logic
  • UI tests for LoRA picker/manager screens

Positive Highlights

  1. Consistent Error Handling: Good use of Result types and error propagation
  2. State Management: Proper use of StateFlow and Compose state
  3. Logging: Comprehensive logging at key points
  4. Progress Tracking: Download progress UI is well-implemented
  5. Resource Cleanup: Most paths properly clean up resources

📊 Summary

Critical Issues (must fix):

  • Memory leak in JNI layer (issue #1)
  • Path traversal vulnerability (issue #14)
  • Null pointer risk in C++ (issue #4)

Important Issues (should fix):

  • Thread safety in download (issue #2)
  • File integrity validation (issue #3)
  • JSON parsing approach (issue #11)

Suggestions (consider):

  • API design improvements (issues #8, #9)
  • Performance optimizations (issue #10)
  • UI/UX enhancements (issues #6, #7)

Overall Assessment: This is a well-structured feature addition with good architectural decisions. The main concerns are around memory safety in the JNI layer and security in file handling. Once the critical issues are addressed, this will be a solid implementation.

Would you like me to create GitHub issues for the critical items to track them separately?

Copy link
Copy Markdown
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

129 files reviewed, 4 comments

Edit Code Review Agent Settings | Greptile

Comment on lines +23 to +31
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)")
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +100 to +131
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",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +180 to +204
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)) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

Suggested change
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.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Feb 24, 2026

Additional Comments (1)

examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/chat/ChatScreen.kt
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.

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: 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.

@shubhammalhotra28
Copy link
Copy Markdown
Contributor

@coderabbitai please do a detailed review. Just added some changes for ios + refactored few things. So, do detailed review. Thanks!

@shubhammalhotra28
Copy link
Copy Markdown
Contributor

@greptile please do a detailed review. Just added some changes for ios + refactored few things. So, do detailed review. Thanks!

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 24, 2026

@shubhammalhotra28, I'll trigger a comprehensive review of the latest changes, including the iOS additions and refactorings you mentioned. Let me analyze the current state of the PR.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

142 files reviewed, 4 comments

Edit Code Review Agent Settings | Greptile

var downloadComplete = false
try {
withContext(Dispatchers.IO) {
val connection = URL(entry.downloadUrl).openConnection().apply {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 | 🟠 Major

Avoid generic Exception handling; use a sealed error type.
Catching Exception reduces 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 | 🟠 Major

Avoid 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 | 🟠 Major

Avoid 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 | 🟡 Minor

Rename Android-specific service to include the Android prefix.

This class is Android-only (AudioRecord, MediaRecorder) but its name omits the required Android prefix. Consider renaming the class/file to AndroidAudioCaptureService and 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 | 🟠 Major

Close 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 trigger awaitClose cleanup, 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:

  1. 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.
  2. All fileSize values are hardcoded to 690_176 but 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 synchronous File.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 LoraUiState during refresh() 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 | 🟡 Minor

Reset scale when default changes.

Line 226 only keys remember by entry.id, so the slider won’t reset if entry.defaultScale changes 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 | 🟡 Minor

Move 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 | 🟡 Minor

Conditionally apply assistant-icon padding in AnalyticsFooter.

Line 928 always reserves icon padding; when message.modelInfo is 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 | 🟠 Major

Avoid JSON string marshaling for LoRA entries in JNI.
loraEntryToJson and the registry getters return JSON strings, which conflicts with the structured‑types guideline. Prefer returning typed JNI objects/arrays (e.g., a LoraEntry class 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 | 🟠 Major

Add JNI wrappers for the remaining LoRA registry C APIs.
rac_lora_registry_remove and rac_lora_registry_get are 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 _jni suffix 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 | 🟠 Major

Expand 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_entry silently returns partially-initialized entry on inner allocation failure.

This was previously flagged: when malloc for compatible_model_ids (Line 34) fails, the function returns a non-null copy with compatible_model_ids = nullptr and compatible_model_count = 0, silently dropping the compatibility list. Similarly, rac_strdup failures on Lines 28–32 are not checked. All callers only check if (!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 @return docs 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, and rac_lora_registry_get_for_model, the implementation can return RAC_ERROR_INVALID_ARGUMENT and RAC_ERROR_OUT_OF_MEMORY, but the docs only say "or error code". The remove and get functions 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. Since parseInlineMarkdown isn't a @Composable, it can't access MaterialTheme — but you could pass a linkColor parameter alongside defaultColor.

♻️ 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 @Composable scope), 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.

drawBatchedByColor already draws the batched points; the preceding drawPoints with Color.Transparent adds 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.

drawBatchedByColor builds a new HashMap and per-bucket ArrayLists 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: Unstructured Task nested inside MainActor.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 retains self. Since LLMViewModel is @MainActor, the await MainActor.run { } wrapper is also redundant here — the entire outer Task {} (line 519) already inherits the main-actor context via @MainActor on 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 @MainActor class, MainActor.run is a no-op here and the refreshAvailableAdapters() 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: Prefer Logger over bare print for error reporting.

Every other file in this module (LLMViewModel.swift, etc.) uses os.log.Logger. Using print here means registration failures are invisible in Console.app, crash logs, and release builds' log streams.

♻️ 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 in LoraUiState.

String errors + generic Exception handling 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 to commonMain.

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 a Jvm* 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

📥 Commits

Reviewing files that changed from the base of the PR and between 00c73b4 and 657fa34.

📒 Files selected for processing (65)
  • .idea/vcs.xml
  • Package.swift
  • examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/MainActivity.kt
  • examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/RunAnywhereApplication.kt
  • examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/data/ConversationStore.kt
  • examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/data/ModelList.kt
  • examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/domain/services/AudioCaptureService.kt
  • examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/benchmarks/models/BenchmarkTypes.kt
  • examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/benchmarks/views/BenchmarkDashboardScreen.kt
  • examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/benchmarks/views/BenchmarkDetailScreen.kt
  • examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/chat/ChatScreen.kt
  • examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/chat/ChatViewModel.kt
  • examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/chat/components/MarkdownText.kt
  • examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/chat/components/MessageInput.kt
  • examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/chat/components/ModelRequiredOverlay.kt
  • examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/components/TopBarState.kt
  • examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/lora/LoraAdapterPickerSheet.kt
  • examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/lora/LoraManagerScreen.kt
  • examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/lora/LoraViewModel.kt
  • examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/models/ModelSelectionBottomSheet.kt
  • examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/models/ModelSelectionViewModel.kt
  • examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/navigation/AppNavigation.kt
  • examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/navigation/MoreHubScreen.kt
  • examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/rag/DocumentRAGScreen.kt
  • examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/rag/RAGViewModel.kt
  • examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/settings/SettingsScreen.kt
  • examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/settings/SettingsViewModel.kt
  • examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/settings/ToolSettingsViewModel.kt
  • examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/stt/SpeechToTextScreen.kt
  • examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/stt/SpeechToTextViewModel.kt
  • examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/tts/TextToSpeechScreen.kt
  • examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/tts/TextToSpeechViewModel.kt
  • examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/vision/VLMScreen.kt
  • examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/vision/VLMViewModel.kt
  • examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/vision/VisionHubScreen.kt
  • examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/voice/VoiceAssistantParticleView.kt
  • examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/voice/VoiceAssistantScreen.kt
  • examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/voice/VoiceAssistantViewModel.kt
  • examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/theme/AppColors.kt
  • examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/theme/Dimensions.kt
  • examples/ios/RunAnywhereAI/Package.resolved
  • examples/ios/RunAnywhereAI/RunAnywhereAI.xcodeproj/project.pbxproj
  • examples/ios/RunAnywhereAI/RunAnywhereAI/App/RunAnywhereAIApp.swift
  • examples/ios/RunAnywhereAI/RunAnywhereAI/Features/Chat/Models/DemoLoRAAdapter.swift
  • examples/ios/RunAnywhereAI/RunAnywhereAI/Features/Chat/ViewModels/LLMViewModel.swift
  • examples/ios/RunAnywhereAI/RunAnywhereAI/Features/Chat/Views/ChatInterfaceView.swift
  • sdk/runanywhere-commons/exports/RACommons.exports
  • sdk/runanywhere-commons/include/rac/core/rac_core.h
  • sdk/runanywhere-commons/include/rac/features/llm/rac_llm_component.h
  • sdk/runanywhere-commons/include/rac/infrastructure/model_management/rac_lora_registry.h
  • sdk/runanywhere-commons/scripts/build-ios.sh
  • sdk/runanywhere-commons/src/features/llm/llm_component.cpp
  • sdk/runanywhere-commons/src/infrastructure/model_management/lora_registry.cpp
  • sdk/runanywhere-commons/src/jni/runanywhere_commons_jni.cpp
  • sdk/runanywhere-kotlin/src/jvmAndroidMain/kotlin/com/runanywhere/sdk/foundation/bridge/extensions/CppBridgeLoraRegistry.kt
  • sdk/runanywhere-kotlin/src/jvmAndroidMain/kotlin/com/runanywhere/sdk/public/extensions/RunAnywhere+LoRA.jvmAndroid.kt
  • sdk/runanywhere-swift/Sources/RunAnywhere/CRACommons/include/CRACommons.h
  • sdk/runanywhere-swift/Sources/RunAnywhere/CRACommons/include/rac_core.h
  • sdk/runanywhere-swift/Sources/RunAnywhere/CRACommons/include/rac_llm_component.h
  • sdk/runanywhere-swift/Sources/RunAnywhere/CRACommons/include/rac_lora_registry.h
  • sdk/runanywhere-swift/Sources/RunAnywhere/CRACommons/include/rac_model_types.h
  • sdk/runanywhere-swift/Sources/RunAnywhere/Foundation/Bridge/Extensions/CppBridge+LLM.swift
  • sdk/runanywhere-swift/Sources/RunAnywhere/Foundation/Bridge/Extensions/CppBridge+LoraRegistry.swift
  • sdk/runanywhere-swift/Sources/RunAnywhere/Public/Extensions/LLM/LLMTypes.swift
  • sdk/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

Comment on lines +169 to +188
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,
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 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 5

Repository: RunanywhereAI/runanywhere-sdks

Length of output: 100


🏁 Script executed:

rg "registerModel" --type kt -B 2 -A 10 | head -100

Repository: RunanywhereAI/runanywhere-sdks

Length of output: 100


🏁 Script executed:

rg "registerMultiFileModel" -A 5 | head -50

Repository: RunanywhereAI/runanywhere-sdks

Length of output: 6711


🏁 Script executed:

rg "fun registerModel" -B 2 -A 15 | head -100

Repository: RunanywhereAI/runanywhere-sdks

Length of output: 8824


🏁 Script executed:

find . -name "*.kt" -type f | xargs grep -l "registerMultiFileModel" | head -5

Repository: 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 -200

Repository: 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 30

Repository: 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.

Comment on lines +48 to +51
fun DocumentRAGScreen(
onBack: () -> Unit = {},
viewModel: RAGViewModel = viewModel(),
) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Comment on lines +712 to +716
58RAGBKND02ED16DA40058D03 /* RunAnywhereRAG */ = {
isa = XCSwiftPackageProductDependency;
package = 58E021172E52A86000B722EF /* XCLocalSwiftPackageReference "../../.." */;
productName = RunAnywhereRAG;
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 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
fi

Repository: 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.swift

Repository: 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")!,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Comment on lines +65 to +71
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;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Comment on lines +83 to +95
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;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

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.

Comment on lines +16 to +118
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()
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ 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.

Comment on lines +485 to +490
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)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find sdk -name "*.h" -o -name "*.hpp" | xargs grep -l "rac_lora_entry_t" 2>/dev/null | head -5

Repository: RunanywhereAI/runanywhere-sdks

Length of output: 242


🏁 Script executed:

# Search for the struct definition
rg "typedef struct.*rac_lora_entry" sdk -A 20 -B 2

Repository: 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 -10

Repository: 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 -100

Repository: 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 -80

Repository: 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 -120

Repository: 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.swift

Repository: 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.

Suggested change
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.

shubhammalhotra28 and others added 2 commits February 24, 2026 01:12
- 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>
@sanchitmonga22 sanchitmonga22 marked this pull request as ready for review February 24, 2026 21:38
@sanchitmonga22
Copy link
Copy Markdown
Contributor

@coderabbitai please review

@sanchitmonga22
Copy link
Copy Markdown
Contributor

@greptile please review

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 24, 2026

@sanchitmonga22, I'll review the changes in this PR.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

142 files reviewed, 3 comments

Edit Code Review Agent Settings | Greptile

Comment on lines +202 to +206
val connection = URL(entry.downloadUrl).openConnection().apply {
connectTimeout = 30_000
readTimeout = 60_000
}
connection.connect()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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.

Comment on lines +224 to +230
}
destFile.delete()
if (!tmpFile.renameTo(destFile)) {
tmpFile.delete()
throw Exception("Failed to move downloaded file to final location")
}
downloadComplete = true
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +86 to +95
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,
),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

♻️ Duplicate comments (4)
examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/lora/LoraViewModel.kt (1)

281-283: Check delete() return value and log failures.

file.delete() can fail silently (permissions, open handles). Log a warning or surface a user error if it returns false.

🤖 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 | 🔴 Critical

Dangling pointer in the registry map when deep_copy_lora_entry fails during re-registration.

Line 108 frees it->second but does not erase the map entry. If deep_copy_lora_entry (line 109) then returns nullptr, handle->entries[adapter_id] still holds the freed (dangling) pointer. Any subsequent get, get_all, get_for_model, or remove call 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 new can throw std::bad_alloc across the C ABI boundary.

rac_lora_registry_create is part of an extern "C" surface; an uncaught C++ exception here causes std::terminate. Use new (std::nothrow) and return RAC_ERROR_OUT_OF_MEMORY on 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 | 🟠 Major

Use 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-local std::string adapter_id at 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 the Android prefix.

This file is under an Android-specific path but the class name lacks the Android prefix (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.error stores a String and all catches build ad‑hoc messages. This loses type information and makes error handling brittle across the UI. Consider a sealed LoraError (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

📥 Commits

Reviewing files that changed from the base of the PR and between 657fa34 and b83a6ac.

📒 Files selected for processing (4)
  • examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/lora/LoraAdapterPickerSheet.kt
  • examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/lora/LoraViewModel.kt
  • sdk/runanywhere-commons/include/rac/core/rac_core.h
  • sdk/runanywhere-commons/src/infrastructure/model_management/lora_registry.cpp

Comment on lines +176 to +181
/** 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()
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Comment on lines +185 to +196
fun downloadAdapter(entry: LoraAdapterCatalogEntry) {
if (_uiState.value.downloadingAdapterId != null) return

_uiState.update {
it.copy(
downloadingAdapterId = entry.id,
downloadProgress = 0f,
error = null,
)
}

downloadJob = viewModelScope.launch {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Comment on lines +343 to +350
/**
* @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);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (7)
sdk/runanywhere-commons/src/infrastructure/model_management/lora_registry.cpp (4)

102-114: ⚠️ Potential issue | 🔴 Critical

Dangling pointer in map when deep_copy_lora_entry fails 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_entry at line 109 then returns nullptr (OOM), the function returns RAC_ERROR_OUT_OF_MEMORY but handle->entries[adapter_id] is left holding a dangling pointer. Any subsequent get, get_all, get_for_model, or remove call 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_entry is used before it is defined — confirmed compile error.

deep_copy_lora_entry calls free_lora_entry at lines 40, 48, and 56 for OOM cleanup. free_lora_entry is a static function first defined at line 66. In C++ there is no implicit declaration; calling an undeclared static function 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: Raw std::string map 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 a typedef/using alias) 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 throw std::bad_alloc across the C boundary.

rac_lora_registry_create is called from C-facing code. An uncaught std::bad_alloc crossing an extern "C" boundary invokes std::terminate. Use new (std::nothrow) and return RAC_ERROR_OUT_OF_MEMORY on 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 | 🟡 Minor

Ambiguous NULL-return contract on rac_get_lora_registry still 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 with rac_get_model_registry at 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 NULL and treat it as an initialization failure, or remove the qualifier if the implementation never returns NULL (e.g., if it calls std::abort on 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 | 🟠 Major

Validate 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 under loraDir before 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 | 🟡 Minor

Guard against concurrent download starts

The downloadingAdapterId check 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 name

This 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 callback

This is already coroutine-based; exposing a suspend function 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

📥 Commits

Reviewing files that changed from the base of the PR and between 657fa34 and bababba.

📒 Files selected for processing (15)
  • Package.swift
  • examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/lora/LoraAdapterPickerSheet.kt
  • examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/lora/LoraViewModel.kt
  • examples/ios/RunAnywhereAI/Package.resolved
  • examples/ios/RunAnywhereAI/RunAnywhereAI.xcodeproj/project.pbxproj
  • examples/ios/RunAnywhereAI/RunAnywhereAI/App/RunAnywhereAIApp.swift
  • sdk/runanywhere-commons/CMakeLists.txt
  • sdk/runanywhere-commons/include/rac/core/rac_core.h
  • sdk/runanywhere-commons/include/rac/infrastructure/model_management/rac_model_types.h
  • sdk/runanywhere-commons/scripts/build-ios.sh
  • sdk/runanywhere-commons/src/infrastructure/model_management/lora_registry.cpp
  • sdk/runanywhere-commons/src/infrastructure/model_management/model_registry.cpp
  • sdk/runanywhere-swift/Sources/RunAnywhere/CRACommons/include/CRACommons.h
  • sdk/runanywhere-swift/Sources/RunAnywhere/CRACommons/include/rac_model_types.h
  • voic-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

Comment on lines +31 to +38
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,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ 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 -100

Repository: 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 1

Repository: 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 kt

Repository: 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 -20

Repository: 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).

Comment on lines +45 to +53
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() }
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ 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.

Comment on lines +201 to +231
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
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (4)
sdk/runanywhere-commons/src/infrastructure/model_management/lora_registry.cpp (4)

104-116: ⚠️ Potential issue | 🔴 Critical

Dangling 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_entry returns nullptr, handle->entries[adapter_id] retains a pointer to already-freed memory, causing UB on any subsequent get, get_all, get_for_model, or remove call.

🐛 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 | 🟠 Major

Raw std::string map 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 a std::string adapter_id from raw const 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 all std::string key/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 throw std::bad_alloc across the extern "C" boundary.

This was flagged previously and remains unaddressed. An uncaught C++ exception in an extern "C" function results in std::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 | 🟡 Minor

Fix 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.) uses new (std::nothrow) universally. Change to new (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 after delete handle.

RAC_LOG_DEBUG at Line 99 is called after delete handle. While there is no UB here since the log doesn't dereference handle, the log conceptually describes the destruction that has already occurred and would appear out of order in traces. Move the log before delete.

♻️ 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

📥 Commits

Reviewing files that changed from the base of the PR and between bababba and 6b28b21.

📒 Files selected for processing (2)
  • sdk/runanywhere-commons/src/infrastructure/model_management/lora_registry.cpp
  • sdk/runanywhere-swift/Sources/RunAnywhere/CRACommons/shim.c

Siddhesh2377 and others added 2 commits February 25, 2026 07:16
- 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>
@sanchitmonga22 sanchitmonga22 merged commit 005fda7 into main Feb 25, 2026
14 of 21 checks passed
@Siddhesh2377 Siddhesh2377 deleted the siddhesh/lora-demo branch March 20, 2026 19:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants