Files

1 line
26 KiB
JSON

{"repo": "aradotso/devtools-skills", "files": {"skills/privatelm-cross-platform-llm-client/SKILL.md": {"15f3b4bb67d414852988d3bbd7577080abadb4bc": {"t": 1785595811.3483222, "n": 25107, "h": "78cc254c1f5dd160", "c": "---\nname: privatelm-cross-platform-llm-client\ndescription: A unified Flutter-based AI client supporting local on-device GGUF model inference and cloud API fallback for building privacy-focused LLM applications.\ntriggers:\n - how do I integrate PrivateLM into my Flutter app\n - set up local LLM inference on Android with PrivateLM\n - configure cloud API fallback in PrivateLM\n - run GGUF models on device with Flutter\n - implement multimodal chat with local and cloud models\n - debug local inference issues in PrivateLM\n - optimize PrivateLM for low-end Android devices\n - switch between local and cloud LLM providers\n---\n\n# PrivateLM Cross-Platform LLM Client\n\n> Skill by [ara.so](https://ara.so) — Devtools Skills collection.\n\n## What It Does\n\nPrivateLM is a production-ready Flutter framework for building AI chat applications that seamlessly switch between:\n\n- **Local on-device inference** — GPU-accelerated GGUF model execution via `llama.cpp` (Android/iOS)\n- **Cloud API providers** — OpenAI, Anthropic Claude, Google Gemini, Kimi (Moonshot AI)\n- **Multimodal capabilities** — Text and vision support for both local (Qwen2-VL) and cloud models\n- **Offline-first architecture** — All data persisted locally via Hive; no cloud dependency for local mode\n\n**Key Features:**\n- Auto-detects device RAM/GPU and configures optimal inference parameters\n- Background service integration with Firebase Cloud Messaging\n- Cross-platform (Android full support, iOS via Metal, Web cloud-only)\n- Persistent chat sessions and task management\n\n## Installation\n\n### Add to Flutter Project\n\n```yaml\n# pubspec.yaml\ndependencies:\n flutter:\n sdk: flutter\n get: ^4.6.5\n hive: ^2.2.3\n hive_flutter: ^1.1.0\n dio: ^5.3.2\n http: ^1.1.0\n flutter_background_service: ^5.0.0\n flutter_local_notifications: ^15.1.0\n firebase_core: ^2.15.0\n firebase_messaging: ^14.6.5\n device_info_plus: ^9.0.3\n path_provider: ^2.1.0\n image_picker: ^1.0.2\n permission_handler: ^11.0.0\n\ndev_dependencies:\n flutter_test:\n sdk: flutter\n hive_generator: ^2.0.0\n build_runner: ^2.4.6\n```\n\n### Platform Configuration\n\n**Android (android/app/build.gradle.kts):**\n```kotlin\nandroid {\n namespace = \"com.yourcompany.yourapp\"\n compileSdk = 34\n ndkVersion = \"25.1.8937393\"\n\n defaultConfig {\n minSdk = 28 // Required for llama_flutter_android\n targetSdk = 34\n ndk {\n abiFilters.add(\"arm64-v8a\") // 64-bit ARM only\n }\n }\n\n buildTypes {\n release {\n isMinifyEnabled = true\n isShrinkResources = true\n proguardFiles(\n getDefaultProguardFile(\"proguard-android-optimize.txt\"),\n \"proguard-rules.pro\"\n )\n }\n }\n}\n```\n\n**iOS (ios/Podfile):**\n```ruby\nplatform :ios, '12.0'\n\ntarget 'Runner' do\n use_frameworks!\n use_modular_headers!\n \n # Ensure Metal framework for GPU acceleration\n pod 'MetalKit'\nend\n```\n\n### Initialize the App\n\n```dart\n// lib/main.dart\nimport 'package:flutter/material.dart';\nimport 'package:get/get.dart';\nimport 'package:hive_flutter/hive_flutter.dart';\nimport 'package:firebase_core/firebase_core.dart';\nimport 'services/hive_service.dart';\nimport 'services/device_info_service.dart';\nimport 'controllers/settings_controller.dart';\nimport 'controllers/model_controller.dart';\nimport 'controllers/chat_controller.dart';\n\nvoid main() async {\n WidgetsFlutterBinding.ensureInitialized();\n \n // Initialize Firebase\n await Firebase.initializeApp();\n \n // Initialize Hive\n await Hive.initFlutter();\n await HiveService.init();\n \n // Initialize GetX controllers\n Get.put(DeviceInfoService());\n Get.put(SettingsController());\n Get.put(ModelController());\n Get.put(ChatController());\n \n runApp(MyApp());\n}\n\nclass MyApp extends StatelessWidget {\n @override\n Widget build(BuildContext context) {\n return GetMaterialApp(\n title: 'PrivateLM Chat',\n theme: ThemeData.dark(),\n home: HomeView(),\n );\n }\n}\n```\n\n## Architecture Overview\n\n### Service Layer\n\n```dart\n// lib/services/inference_service.dart\nimport 'package:get/get.dart';\nimport 'dart:io' show Platform;\n\nclass InferenceService extends GetxService {\n static bool get supportsLocalInference {\n if (Platform.isAndroid) return true;\n if (Platform.isIOS) return true;\n return false; // Web not yet supported\n }\n\n Future<void> loadModel(String modelPath, {\n required int contextSize,\n required int numThreads,\n required int gpuLayers,\n }) async {\n if (!supportsLocalInference) {\n throw UnsupportedError('Local inference not available on this platform');\n }\n \n // Platform-specific implementation\n if (Platform.isAndroid || Platform.isIOS) {\n await _loadModelNative(modelPath, contextSize, numThreads, gpuLayers);\n }\n }\n\n Stream<String> generateChat({\n required List<Map<String, String>> messages,\n required String template,\n int maxTokens = 512,\n double temperature = 0.7,\n }) async* {\n // Streaming token generation\n // Implementation calls native llama.cpp bridge\n }\n}\n```\n\n### Cloud Service Integration\n\n```dart\n// lib/services/cloud_service.dart\nimport 'package:dio/dio.dart';\nimport 'package:get/get.dart';\n\nclass CloudService extends GetxService {\n final Dio _dio = Dio();\n\n // OpenAI-compatible endpoint\n Stream<String> generateOpenAI({\n required String apiKey,\n required List<Map<String, dynamic>> messages,\n String model = 'gpt-4',\n int maxTokens = 1024,\n double temperature = 0.7,\n }) async* {\n final response = await _dio.post(\n 'https://api.openai.com/v1/chat/completions',\n options: Options(\n headers: {\n 'Authorization': 'Bearer $apiKey',\n 'Content-Type': 'application/json',\n },\n ),\n data: {\n 'model': model,\n 'messages': messages,\n 'max_tokens': maxTokens,\n 'temperature': temperature,\n 'stream': true,\n },\n );\n \n // Parse SSE stream\n await for (final chunk in response.data.stream) {\n yield _parseOpenAIChunk(chunk);\n }\n }\n\n // Anthropic Claude endpoint\n Stream<String> generateClaude({\n required String apiKey,\n required List<Map<String, dynamic>> messages,\n String model = 'claude-3-opus-20240229',\n int maxTokens = 1024,\n }) async* {\n // Extract system message\n String? systemMessage;\n final userMessages = messages.where((m) {\n if (m['role'] == 'system') {\n systemMessage = m['content'];\n return false;\n }\n return true;\n }).toList();\n\n final response = await _dio.post(\n 'https://api.anthropic.com/v1/messages',\n options: Options(\n headers: {\n 'x-api-key': apiKey,\n 'anthropic-version': '2023-06-01',\n 'Content-Type': 'application/json',\n },\n ),\n data: {\n 'model': model,\n 'max_tokens': maxTokens,\n 'messages': userMessages,\n if (systemMessage != null) 'system': systemMessage,\n 'stream': true,\n },\n );\n \n await for (final chunk in response.data.stream) {\n yield _parseClaudeChunk(chunk);\n }\n }\n\n // Google Gemini endpoint\n Stream<String> generateGemini({\n required String apiKey,\n required List<Map<String, dynamic>> messages,\n String model = 'gemini-1.5-pro',\n }) async* {\n final contents = messages.map((m) => {\n 'role': m['role'] == 'assistant' ? 'model' : 'user',\n 'parts': [{'text': m['content']}],\n }).toList();\n\n final response = await _dio.post(\n 'https://generativelanguage.googleapis.com/v1beta/models/$model:streamGenerateContent?key=$apiKey',\n options: Options(\n headers: {'Content-Type': 'application/json'},\n ),\n data: {'contents': contents},\n );\n \n await for (final chunk in response.data.stream) {\n yield _parseGeminiChunk(chunk);\n }\n }\n}\n```\n\n## Device Detection & Auto-Configuration\n\n```dart\n// lib/services/device_info_service.dart\nimport 'package:device_info_plus/device_info_plus.dart';\nimport 'package:get/get.dart';\nimport 'dart:io';\n\nenum DeviceTier { ultra, high, mid, low }\n\nclass DeviceInfoService extends GetxService {\n final DeviceInfoPlugin _deviceInfo = DeviceInfoPlugin();\n \n DeviceTier? _tier;\n int? _totalRamMB;\n \n DeviceTier get tier => _tier ?? DeviceTier.mid;\n int get totalRamMB => _totalRamMB ?? 4096;\n \n Future<void> detectDevice() async {\n if (Platform.isAndroid) {\n final androidInfo = await _deviceInfo.androidInfo;\n _totalRamMB = _estimateAndroidRAM(androidInfo);\n } else if (Platform.isIOS) {\n final iosInfo = await _deviceInfo.iosInfo;\n _totalRamMB = _estimateIOSRAM(iosInfo);\n }\n \n _tier = _calculateTier(_totalRamMB!);\n }\n \n DeviceTier _calculateTier(int ramMB) {\n if (ramMB >= 12000) return DeviceTier.ultra; // 12GB+\n if (ramMB >= 8000) return DeviceTier.high; // 8-12GB\n if (ramMB >= 6000) return DeviceTier.mid; // 6-8GB\n return DeviceTier.low; // <6GB\n }\n \n Map<String, int> getOptimalSettings() {\n switch (tier) {\n case DeviceTier.ultra:\n return {\n 'contextSize': 8192,\n 'threads': 8,\n 'gpuLayers': 33,\n 'batchSize': 512,\n };\n case DeviceTier.high:\n return {\n 'contextSize': 4096,\n 'threads': 6,\n 'gpuLayers': 25,\n 'batchSize': 256,\n };\n case DeviceTier.mid:\n return {\n 'contextSize': 2048,\n 'threads': 4,\n 'gpuLayers': 15,\n 'batchSize': 128,\n };\n case DeviceTier.low:\n return {\n 'contextSize': 1024,\n 'threads': 2,\n 'gpuLayers': 8,\n 'batchSize': 64,\n };\n }\n }\n}\n```\n\n## Local Model Management\n\n```dart\n// lib/controllers/model_controller.dart\nimport 'package:get/get.dart';\nimport 'package:dio/dio.dart';\nimport 'package:path_provider/path_provider.dart';\nimport 'dart:io';\n\nclass ModelController extends GetxController {\n final RxList<LocalModel> models = <LocalModel>[].obs;\n final RxMap<String, double> downloadProgress = <String, double>{}.obs;\n \n Future<void> downloadModel({\n required String url,\n required String modelName,\n required String fileName,\n }) async {\n final appDir = await getApplicationDocumentsDirectory();\n final modelDir = Directory('${appDir.path}/models');\n if (!await modelDir.exists()) {\n await modelDir.create(recursive: true);\n }\n \n final filePath = '${modelDir.path}/$fileName';\n \n try {\n await Dio().download(\n url,\n filePath,\n onReceiveProgress: (received, total) {\n if (total != -1) {\n final progress = received / total;\n downloadProgress[modelName] = progress;\n }\n },\n );\n \n // Save model metadata\n final model = LocalModel(\n name: modelName,\n path: filePath,\n sizeBytes: File(filePath).lengthSync(),\n downloadDate: DateTime.now(),\n );\n \n models.add(model);\n await _saveModelsToHive();\n \n Get.snackbar('Success', '$modelName downloaded successfully');\n } catch (e) {\n Get.snackbar('Error', 'Failed to download $modelName: $e');\n downloadProgress.remove(modelName);\n }\n }\n \n Future<void> deleteModel(String modelName) async {\n final model = models.firstWhere((m) => m.name == modelName);\n final file = File(model.path);\n \n if (await file.exists()) {\n await file.delete();\n }\n \n models.removeWhere((m) => m.name == modelName);\n await _saveModelsToHive();\n }\n}\n\nclass LocalModel {\n final String name;\n final String path;\n final int sizeBytes;\n final DateTime downloadDate;\n \n LocalModel({\n required this.name,\n required this.path,\n required this.sizeBytes,\n required this.downloadDate,\n });\n}\n```\n\n## Chat Implementation\n\n```dart\n// lib/controllers/chat_controller.dart\nimport 'package:get/get.dart';\nimport '../services/inference_service.dart';\nimport '../services/cloud_service.dart';\nimport '../services/hive_service.dart';\n\nclass ChatController extends GetxController {\n final InferenceService _inference = Get.find();\n final CloudService _cloud = Get.find();\n final HiveService _hive = Get.find();\n \n final RxList<ChatMessage> messages = <ChatMessage>[].obs;\n final RxBool isGenerating = false.obs;\n final RxString currentProvider = 'local'.obs; // 'local', 'openai', 'claude', etc.\n \n Future<void> sendMessage(String text, {String? imagePath}) async {\n // Add user message\n final userMsg = ChatMessage(\n role: 'user',\n content: text,\n timestamp: DateTime.now(),\n imagePath: imagePath,\n );\n messages.add(userMsg);\n \n // Create assistant message placeholder\n final assistantMsg = ChatMessage(\n role: 'assistant',\n content: '',\n timestamp: DateTime.now(),\n );\n messages.add(assistantMsg);\n \n isGenerating.value = true;\n \n try {\n if (currentProvider.value == 'local') {\n await _generateLocal(assistantMsg);\n } else {\n await _generateCloud(assistantMsg);\n }\n } catch (e) {\n assistantMsg.content = 'Error: $e';\n } finally {\n isGenerating.value = false;\n await _saveConversation();\n }\n }\n \n Future<void> _generateLocal(ChatMessage assistantMsg) async {\n final chatHistory = messages\n .where((m) => m.role != 'system')\n .map((m) => {\n 'role': m.role,\n 'content': m.content,\n })\n .toList();\n \n await for (final token in _inference.generateChat(\n messages: chatHistory,\n template: 'chatml', // or 'llama3', 'gemma', 'phi'\n maxTokens: 512,\n temperature: 0.7,\n )) {\n assistantMsg.content += token;\n messages.refresh(); // Trigger UI update\n }\n }\n \n Future<void> _generateCloud(ChatMessage assistantMsg) async {\n final apiKey = _hive.getApiKey(currentProvider.value);\n if (apiKey == null) {\n throw Exception('API key not configured for ${currentProvider.value}');\n }\n \n final chatHistory = messages\n .where((m) => m != assistantMsg)\n .map((m) => {\n 'role': m.role,\n 'content': m.content,\n })\n .toList();\n \n Stream<String> stream;\n \n switch (currentProvider.value) {\n case 'openai':\n stream = _cloud.generateOpenAI(\n apiKey: apiKey,\n messages: chatHistory,\n model: 'gpt-4',\n );\n break;\n case 'claude':\n stream = _cloud.generateClaude(\n apiKey: apiKey,\n messages: chatHistory,\n model: 'claude-3-opus-20240229',\n );\n break;\n case 'gemini':\n stream = _cloud.generateGemini(\n apiKey: apiKey,\n messages: chatHistory,\n );\n break;\n default:\n throw Exception('Unknown provider: ${currentProvider.value}');\n }\n \n await for (final token in stream) {\n assistantMsg.content += token;\n messages.refresh();\n }\n }\n \n Future<void> switchProvider(String provider) async {\n currentProvider.value = provider;\n \n // If switching to local, ensure model is loaded\n if (provider == 'local') {\n final modelController = Get.find<ModelController>();\n if (modelController.models.isEmpty) {\n Get.snackbar('Warning', 'No local models available. Download a model first.');\n }\n }\n }\n}\n\nclass ChatMessage {\n final String role;\n String content;\n final DateTime timestamp;\n final String? imagePath;\n \n ChatMessage({\n required this.role,\n required this.content,\n required this.timestamp,\n this.imagePath,\n });\n}\n```\n\n## Multimodal (Vision) Support\n\n```dart\n// lib/services/vision_service.dart\nimport 'dart:convert';\nimport 'dart:io';\nimport 'package:get/get.dart';\n\nclass VisionService extends GetxService {\n // For local models (Qwen2-VL)\n Future<String> processImageLocal(String imagePath, String prompt) async {\n final bytes = await File(imagePath).readAsBytes();\n final base64Image = base64Encode(bytes);\n \n final inference = Get.find<InferenceService>();\n \n final messages = [\n {\n 'role': 'user',\n 'content': [\n {'type': 'image', 'data': base64Image},\n {'type': 'text', 'text': prompt},\n ],\n },\n ];\n \n String response = '';\n await for (final token in inference.generateChat(\n messages: messages,\n template: 'qwen2vl',\n )) {\n response += token;\n }\n \n return response;\n }\n \n // For cloud providers (OpenAI GPT-4V, Gemini)\n Future<String> processImageCloud({\n required String provider,\n required String apiKey,\n required String imagePath,\n required String prompt,\n }) async {\n final bytes = await File(imagePath).readAsBytes();\n final base64Image = base64Encode(bytes);\n \n final cloud = Get.find<CloudService>();\n \n if (provider == 'openai') {\n final messages = [\n {\n 'role': 'user',\n 'content': [\n {'type': 'text', 'text': prompt},\n {\n 'type': 'image_url',\n 'image_url': {\n 'url': 'data:image/jpeg;base64,$base64Image',\n },\n },\n ],\n },\n ];\n \n String response = '';\n await for (final token in cloud.generateOpenAI(\n apiKey: apiKey,\n messages: messages,\n model: 'gpt-4-vision-preview',\n )) {\n response += token;\n }\n return response;\n } else if (provider == 'gemini') {\n // Gemini handles images inline\n final messages = [\n {\n 'role': 'user',\n 'content': prompt,\n 'image': base64Image,\n },\n ];\n \n String response = '';\n await for (final token in cloud.generateGemini(\n apiKey: apiKey,\n messages: messages,\n )) {\n response += token;\n }\n return response;\n }\n \n throw Exception('Vision not supported for provider: $provider');\n }\n}\n```\n\n## Settings & Configuration\n\n```dart\n// lib/controllers/settings_controller.dart\nimport 'package:get/get.dart';\nimport '../services/hive_service.dart';\nimport '../services/device_info_service.dart';\n\nclass SettingsController extends GetxController {\n final HiveService _hive = Get.find();\n final DeviceInfoService _deviceInfo = Get.find();\n \n // Inference settings\n final RxInt contextSize = 2048.obs;\n final RxInt numThreads = 4.obs;\n final RxInt gpuLayers = 15.obs;\n final RxInt maxTokens = 512.obs;\n final RxDouble temperature = 0.7.obs;\n \n // API keys\n final RxMap<String, String> apiKeys = <String, String>{}.obs;\n \n @override\n void onInit() {\n super.onInit();\n loadSettings();\n }\n \n Future<void> loadSettings() async {\n // Load from Hive or use auto-detected defaults\n final savedSettings = _hive.getSettings();\n \n if (savedSettings != null) {\n contextSize.value = savedSettings['contextSize'];\n numThreads.value = savedSettings['numThreads'];\n gpuLayers.value = savedSettings['gpuLayers'];\n maxTokens.value = savedSettings['maxTokens'];\n temperature.value = savedSettings['temperature'];\n } else {\n // Auto-configure based on device\n await _deviceInfo.detectDevice();\n final optimal = _deviceInfo.getOptimalSettings();\n \n contextSize.value = optimal['contextSize']!;\n numThreads.value = optimal['threads']!;\n gpuLayers.value = optimal['gpuLayers']!;\n maxTokens.value = 512;\n temperature.value = 0.7;\n \n await saveSettings();\n }\n \n // Load API keys\n apiKeys.value = _hive.getAllApiKeys();\n }\n \n Future<void> saveSettings() async {\n await _hive.saveSettings({\n 'contextSize': contextSize.value,\n 'numThreads': numThreads.value,\n 'gpuLayers': gpuLayers.value,\n 'maxTokens': maxTokens.value,\n 'temperature': temperature.value,\n });\n }\n \n Future<void> setApiKey(String provider, String key) async {\n apiKeys[provider] = key;\n await _hive.saveApiKey(provider, key);\n }\n \n void resetToAutoDetected() {\n final optimal = _deviceInfo.getOptimalSettings();\n contextSize.value = optimal['contextSize']!;\n numThreads.value = optimal['threads']!;\n gpuLayers.value = optimal['gpuLayers']!;\n saveSettings();\n }\n}\n```\n\n## Configuration File Example\n\n```dart\n// lib/config/app_config.dart\nclass AppConfig {\n // Cloud API endpoints\n static const String openaiEndpoint = 'https://api.openai.com/v1';\n static const String anthropicEndpoint = 'https://api.anthropic.com/v1';\n static const String geminiEndpoint = 'https://generativelanguage.googleapis.com/v1beta';\n static const String kimiEndpoint = 'https://api.moonshot.cn/v1';\n \n // Model recommendations by device tier\n static const Map<String, List<String>> recommendedModels = {\n 'ultra': [\n 'Qwen2.5-14B-Q5_K_M.gguf',\n 'Llama-3.2-11B-Vision-Q5_K_M.gguf',\n 'Mistral-7B-Instruct-Q8_0.gguf',\n ],\n 'high': [\n 'Qwen2.5-7B-Q5_K_M.gguf',\n 'Phi-3.5-mini-instruct-Q6_K.gguf',\n 'Gemma-2-9B-Q4_K_M.gguf',\n ],\n 'mid': [\n 'Qwen2-3B-Q5_K_M.gguf',\n 'Phi-3-mini-Q4_K_M.gguf',\n 'TinyLlama-1.1B-Q8_0.gguf',\n ],\n 'low': [\n 'Qwen2-1.5B-Q4_K_M.gguf',\n 'TinyLlama-1.1B-Q4_K_M.gguf',\n ],\n };\n \n // Chat templates\n static const Map<String, String> chatTemplates = {\n 'chatml': '<|im_start|>system\\n{system}<|im_end|>\\n<|im_start|>user\\n{user}<|im_end|>\\n<|im_start|>assistant\\n',\n 'llama3': '<|begin_of_text|><|start_header_id|>system<|end_header_id|>\\n{system}<|eot_id|><|start_header_id|>user<|end_header_id|>\\n{user}<|eot_id|><|start_header_id|>assistant<|end_header_id|>',\n 'gemma': '<bos><start_of_turn>user\\n{user}<end_of_turn>\\n<start_of_turn>model\\n',\n 'phi': '<|system|>\\n{system}<|end|>\\n<|user|>\\n{user}<|end|>\\n<|assistant|>',\n };\n \n // Environment variable keys\n static const String envOpenAIKey = 'OPENAI_API_KEY';\n static const String envAnthropicKey = 'ANTHROPIC_API_KEY';\n static const String envGeminiKey = 'GEMINI_API_KEY';\n static const String envKimiKey = 'KIMI_API_KEY';\n}\n```\n\n## Common Patterns\n\n### Pattern 1: Fallback from Local to Cloud\n\n```dart\nFuture<String> generateWithFallback(String prompt) async {\n final chatController = Get.find<ChatController>();\n \n try {\n // Try local first\n chatController.switchProvider('local');\n await chatController.sendMessage(prompt);\n return chatController.messages.last.content;\n } catch (localError) {\n print('Local inference failed: $localError');\n \n // Fallback to cloud\n try {\n chatController.switchProvider('openai');\n await chatController.sendMessage(prompt);\n return chatController.messages.last.content;\n } catch (cloudError) {\n throw Exception('Both local and cloud inference failed');\n }\n }\n}\n```\n\n### Pattern 2: Batch Processing with Progress\n\n```dart\nFuture<void> processBatchQuestions(List<String> questions) async {\n final chatController = Get.find<ChatController>();\n final results = <String>[];\n \n for (int i = 0; i < questions.length; i++) {\n await chatController.sendMessage(questions[i]);\n results.add(chatController.messages.last.content);\n \n // Update progress\n print('Processed ${i + 1}/${questions.length}');\n \n // Rate limiting for cloud APIs\n if (chatController.currentProvider.value != 'local') {\n await Future.delayed(Duration(seconds: 1));\n }\n }\n \n return results;\n}\n```\n\n### Pattern 3: Custom Model Loading with Validation\n\n```dart\nFuture<void> loadAndValidateModel(String modelPath) async {\n final inference = Get.find<InferenceService>();\n final settings = Get.find<SettingsController>();\n \n // Check file exists\n final file = File(modelPath);\n if (!await file.exists()) {\n throw Exception('Model file not found: $modelPath');\n }\n \n // Check file size vs available RAM\n final fileSizeMB = (await file.length()) / (1024 * 1024);\n final deviceInfo = Get.find<DeviceInfoService>();\n final availableRAM = deviceInfo.totalRamMB * 0.7; // Use 70% max\n \n if (fileSizeMB > availableRAM) {\n throw Exception('Model too large for device. Need ${fileSizeMB}MB, have ${availableRAM}MB');\n }\n \n // Load with optimal settings\n await inference.loadModel(\n modelPath,\n contextSize: settings.contextSize.value,\n numThreads: settings.numThreads.value,\n gpuLayers: settings.gpuLayers.value,\n );\n \n // Test generation\n final testResult = await inference.generateChat(\n messages: [{'role': 'user', 'content': 'Hi'}],\n template: 'chatml',\n maxTokens: 10,\n ).first;\n \n if (testResult.isEmpty) {\n throw Exception('Model loaded but failed to generate');\n }\n \n print('Model validated successfully');\n}\n```\n\n### Pattern 4: Persistent Chat Sessions\n\n```dart\n// Save and restore conversations\nclass ChatSessionManager {\n final HiveService _hive = Get.find();\n \n Future<void> saveSession(String sessionId, List<ChatMessage> messages) async {\n final data = messages.map((m) => {\n \n"}}}}