|
1 | 1 | package com.itsaky.androidide.plugins.aicore |
2 | 2 |
|
| 3 | +import android.app.ActivityManager |
| 4 | +import android.content.Context |
3 | 5 | import android.llama.cpp.LLamaAndroid |
4 | 6 | import android.net.Uri |
5 | 7 | import android.provider.OpenableColumns |
@@ -61,6 +63,9 @@ class LocalLlmBackend(private val context: PluginContext) : LlmBackend, Cancella |
61 | 63 | @Volatile private var currentStreamingJob: Job? = null |
62 | 64 | @Volatile private var currentGenerateJob: Job? = null |
63 | 65 |
|
| 66 | + /** Renders load diagnoses as user-facing text; keeps R.string lookups out of this engine. */ |
| 67 | + private val loadMessages by lazy { ModelLoadMessages(context.androidContext) } |
| 68 | + |
64 | 69 | @Volatile private var modelLoaded = false |
65 | 70 | @Volatile private var currentModelPath: String? = null |
66 | 71 |
|
@@ -212,6 +217,13 @@ class LocalLlmBackend(private val context: PluginContext) : LlmBackend, Cancella |
212 | 217 | } |
213 | 218 | } |
214 | 219 |
|
| 220 | + /** |
| 221 | + * Loads [modelPath] unless it is already resident, diagnosing any native failure into a |
| 222 | + * [ModelLoadException]. Cancellation is rethrown first because [CancellationException] extends |
| 223 | + * [IllegalStateException] and would otherwise be diagnosed as a corrupt model. |
| 224 | + * |
| 225 | + * @param modelPath the configured model path or content URI |
| 226 | + */ |
215 | 227 | private suspend fun ensureModelLoaded(modelPath: String) { |
216 | 228 | // Resolve content URI to actual file path |
217 | 229 | val resolvedPath = resolveContentUriToPath(modelPath) |
@@ -242,14 +254,36 @@ class LocalLlmBackend(private val context: PluginContext) : LlmBackend, Cancella |
242 | 254 | currentModelPath = null |
243 | 255 | } |
244 | 256 |
|
245 | | - // Load new model with resolved path |
246 | 257 | context.logger.info("Loading model: $resolvedPath") |
247 | | - llama.load(resolvedPath) |
| 258 | + try { |
| 259 | + llama.load(resolvedPath) |
| 260 | + } catch (e: CancellationException) { |
| 261 | + throw e |
| 262 | + } catch (e: Exception) { |
| 263 | + if (e is UserActionableLlmException) throw e |
| 264 | + // Native load_model() signals failure only with a null handle, so diagnose the likely cause. |
| 265 | + context.logger.error("Native model load failed for $resolvedPath", e) |
| 266 | + val diagnosis = ModelLoadDiagnostics.diagnose(resolvedPath, availableMemoryBytes(), e.message) |
| 267 | + throw ModelLoadException(loadMessages.describe(diagnosis), diagnosis) |
| 268 | + } |
248 | 269 | modelLoaded = true |
249 | 270 | currentModelPath = resolvedPath |
250 | 271 | context.logger.info("Model loaded successfully") |
251 | 272 | } |
252 | 273 |
|
| 274 | + /** |
| 275 | + * @return free RAM the OS reports, or -1 if unreadable (diagnosis then skips the low-memory case) |
| 276 | + */ |
| 277 | + private fun availableMemoryBytes(): Long = try { |
| 278 | + val am = context.androidContext.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager |
| 279 | + val info = ActivityManager.MemoryInfo() |
| 280 | + am.getMemoryInfo(info) |
| 281 | + info.availMem |
| 282 | + } catch (e: Exception) { |
| 283 | + context.logger.warn("Could not read available memory: ${e.message}") |
| 284 | + -1L |
| 285 | + } |
| 286 | + |
253 | 287 | /** |
254 | 288 | * Wraps a turn in ChatML — the prompt format Qwen and most other on-device instruct GGUFs |
255 | 289 | * were fine-tuned on. |
@@ -369,7 +403,7 @@ class LocalLlmBackend(private val context: PluginContext) : LlmBackend, Cancella |
369 | 403 | throw ce |
370 | 404 | } catch (e: Exception) { |
371 | 405 | context.logger.error("Error during generation", e) |
372 | | - if (e is ModelNotConfiguredException || e is IncompatibleModelException) { |
| 406 | + if (e is UserActionableLlmException) { |
373 | 407 | UserFeedback.notify(context.androidContext, e.message ?: "Local LLM is not configured.") |
374 | 408 | } |
375 | 409 | future.complete(LlmResponse.failure("Error: ${e.message}")) |
@@ -474,7 +508,7 @@ class LocalLlmBackend(private val context: PluginContext) : LlmBackend, Cancella |
474 | 508 | throw ce |
475 | 509 | } catch (e: Exception) { |
476 | 510 | context.logger.error("Error during streaming generation", e) |
477 | | - if (e is ModelNotConfiguredException || e is IncompatibleModelException) { |
| 511 | + if (e is UserActionableLlmException) { |
478 | 512 | UserFeedback.notify(context.androidContext, e.message ?: "Local LLM is not configured.") |
479 | 513 | } |
480 | 514 | callback.onError("Error: ${e.message}") |
|
0 commit comments