diff --git a/Package.swift b/Package.swift index 40b8d620d..274b56fa0 100644 --- a/Package.swift +++ b/Package.swift @@ -40,7 +40,7 @@ let onnxRuntimeMacOSPath = "\(packageDir)/sdk/runanywhere-swift/Binaries/onnxrun // ./scripts/build-swift.sh --set-remote (sets useLocalBinaries = false) // // ============================================================================= -let useLocalBinaries = false // Toggle: true for local dev, false for release +let useLocalBinaries = false // Toggle: true for local dev, false for release // Version for remote XCFrameworks (used when testLocal = false) // Updated automatically by CI/CD during releases diff --git a/examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/chat/ChatViewModel.kt b/examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/chat/ChatViewModel.kt index d8743e1ae..52edb75a7 100644 --- a/examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/chat/ChatViewModel.kt +++ b/examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/chat/ChatViewModel.kt @@ -80,6 +80,10 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) { private var generationJob: Job? = null + private val generationPrefs by lazy { + getApplication().getSharedPreferences("generation_settings", android.content.Context.MODE_PRIVATE) + } + init { // Always start with a new conversation for a fresh chat experience val conversation = conversationStore.createConversation() @@ -342,7 +346,7 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) { try { // Use SDK streaming generation - returns Flow - RunAnywhere.generateStream(prompt).collect { token -> + RunAnywhere.generateStream(prompt, getGenerationOptions()).collect { token -> fullResponse += token totalTokensReceived++ @@ -466,7 +470,7 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) { try { // RunAnywhere.generate() returns LLMGenerationResult - val result = RunAnywhere.generate(prompt) + val result = RunAnywhere.generate(prompt, getGenerationOptions()) val response = result.text val endTime = System.currentTimeMillis() @@ -846,6 +850,25 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) { _uiState.value = _uiState.value.copy(error = null) } + /** + * Get generation options from SharedPreferences + */ + private fun getGenerationOptions(): com.runanywhere.sdk.public.extensions.LLM.LLMGenerationOptions { + val temperature = generationPrefs.getFloat("defaultTemperature", 0.7f) + val maxTokens = generationPrefs.getInt("defaultMaxTokens", 1000) + val systemPromptValue = generationPrefs.getString("defaultSystemPrompt", "") + val systemPrompt = if (systemPromptValue.isNullOrEmpty()) null else systemPromptValue + val systemPromptInfo = systemPrompt?.let { "set(${it.length} chars)" } ?: "nil" + + Log.i(TAG, "[PARAMS] App getGenerationOptions: temperature=$temperature, maxTokens=$maxTokens, systemPrompt=$systemPromptInfo") + + return com.runanywhere.sdk.public.extensions.LLM.LLMGenerationOptions( + maxTokens = maxTokens, + temperature = temperature, + systemPrompt = systemPrompt + ) + } + /** * Format a ToolValue map to JSON string for display. * Uses kotlinx.serialization for proper JSON escaping of special characters. diff --git a/examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/settings/SettingsScreen.kt b/examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/settings/SettingsScreen.kt index a097ba54c..328faff07 100644 --- a/examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/settings/SettingsScreen.kt +++ b/examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/settings/SettingsScreen.kt @@ -2,6 +2,7 @@ package com.runanywhere.runanywhereai.presentation.settings +import android.app.Application import android.content.Intent import android.net.Uri import android.text.format.Formatter @@ -31,13 +32,12 @@ import androidx.lifecycle.viewmodel.compose.viewModel import com.runanywhere.runanywhereai.ui.theme.AppColors import com.runanywhere.runanywhereai.ui.theme.AppTypography import com.runanywhere.runanywhereai.ui.theme.Dimensions -import android.app.Application /** - * Settings screen + * Settings & Storage Screen * - * Section order: Generation Settings, API Configuration, Storage Overview, Downloaded Models, - * Storage Management, Logging Configuration, About. + * Section order: API Configuration, Generation Settings, Tool Calling, + * Storage Overview, Downloaded Models, Storage Management, Logging Configuration, About. */ @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -72,54 +72,7 @@ fun SettingsScreen(viewModel: SettingsViewModel = viewModel()) { ) } - // 1. Generation Settings - SettingsSection(title = "Generation Settings") { - Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { - Text( - text = "Temperature: ${"%.2f".format(uiState.temperature)}", - style = AppTypography.caption, - color = AppColors.textSecondary, - ) - Slider( - value = uiState.temperature, - onValueChange = { viewModel.updateTemperature(it) }, - valueRange = 0f..2f, - steps = 19, - modifier = Modifier.fillMaxWidth(), - ) - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - text = "Max Tokens: ${uiState.maxTokens}", - style = MaterialTheme.typography.bodyMedium, - ) - Row(verticalAlignment = Alignment.CenterVertically) { - OutlinedButton( - onClick = { viewModel.updateMaxTokens((uiState.maxTokens - 500).coerceAtLeast(500)) }, - contentPadding = PaddingValues(horizontal = 12.dp, vertical = 4.dp), - modifier = Modifier.height(32.dp), - ) { Text("-", style = AppTypography.caption) } - Spacer(modifier = Modifier.width(8.dp)) - Text( - text = "${uiState.maxTokens}", - style = AppTypography.caption, - modifier = Modifier.widthIn(min = 48.dp), - ) - Spacer(modifier = Modifier.width(8.dp)) - OutlinedButton( - onClick = { viewModel.updateMaxTokens((uiState.maxTokens + 500).coerceAtMost(20000)) }, - contentPadding = PaddingValues(horizontal = 12.dp, vertical = 4.dp), - modifier = Modifier.height(32.dp), - ) { Text("+", style = AppTypography.caption) } - } - } - } - } - - // 2. API Configuration (Testing) + // 1. API Configuration (Testing) SettingsSection(title = "API Configuration (Testing)") { Row( modifier = Modifier @@ -182,10 +135,99 @@ fun SettingsScreen(viewModel: SettingsViewModel = viewModel()) { ) } - // Tool Calling Section + // 2. Generation Settings Section + SettingsSection(title = "Generation Settings") { + // Temperature Slider + Column(modifier = Modifier.fillMaxWidth()) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = "Temperature", + style = MaterialTheme.typography.bodyLarge, + ) + Text( + text = String.format("%.1f", uiState.temperature), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + Slider( + value = uiState.temperature, + onValueChange = { viewModel.updateTemperature(it) }, + valueRange = 0f..2f, + steps = 19, // 0.1 increments from 0.0 to 2.0 + modifier = Modifier.fillMaxWidth(), + ) + } + + HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp)) + + // Max Tokens Slider + Column(modifier = Modifier.fillMaxWidth()) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = "Max Tokens", + style = MaterialTheme.typography.bodyLarge, + ) + Text( + text = uiState.maxTokens.toString(), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + Slider( + value = uiState.maxTokens.toFloat(), + onValueChange = { viewModel.updateMaxTokens(it.toInt()) }, + valueRange = 50f..4096f, + steps = 80, // 50-token increments + modifier = Modifier.fillMaxWidth(), + ) + } + + HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp)) + + // System Prompt TextField + OutlinedTextField( + value = uiState.systemPrompt, + onValueChange = { viewModel.updateSystemPrompt(it) }, + label = { Text("System Prompt") }, + placeholder = { Text("Enter system prompt (optional)") }, + modifier = Modifier.fillMaxWidth(), + maxLines = 3, + textStyle = MaterialTheme.typography.bodyMedium, + ) + + Spacer(modifier = Modifier.height(8.dp)) + + // Save Button + OutlinedButton( + onClick = { viewModel.saveGenerationSettings() }, + colors = ButtonDefaults.outlinedButtonColors( + contentColor = AppColors.primaryAccent, + ), + ) { + Text("Save Settings") + } + + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = "These settings affect LLM text generation.", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + + // 3. Tool Calling Section ToolSettingsSection() - // 3. Storage Overview - iOS Label(systemImage: "externaldrive") etc. + // 4. Storage Overview SettingsSection( title = "Storage Overview", trailing = { @@ -223,7 +265,7 @@ fun SettingsScreen(viewModel: SettingsViewModel = viewModel()) { ) } - // 4. Downloaded Models + // 5. Downloaded Models SettingsSection(title = "Downloaded Models") { if (uiState.downloadedModels.isEmpty()) { Text( @@ -245,7 +287,7 @@ fun SettingsScreen(viewModel: SettingsViewModel = viewModel()) { } } - // 5. Storage Management - iOS trash icon, red/orange + // 6. Storage Management SettingsSection(title = "Storage Management") { StorageManagementButton( title = "Clear Cache", @@ -264,7 +306,7 @@ fun SettingsScreen(viewModel: SettingsViewModel = viewModel()) { ) } - // 6. Logging Configuration - iOS Toggle "Log Analytics Locally" + // 7. Logging Configuration SettingsSection(title = "Logging Configuration") { Row( modifier = Modifier.fillMaxWidth(), @@ -288,7 +330,7 @@ fun SettingsScreen(viewModel: SettingsViewModel = viewModel()) { ) } - // 7. About - iOS Label "RunAnywhere SDK" systemImage "cube", "Documentation" systemImage "book" + // 8. About SettingsSection(title = "About") { Row( modifier = Modifier.padding(vertical = 8.dp), @@ -394,7 +436,7 @@ fun SettingsScreen(viewModel: SettingsViewModel = viewModel()) { ) } - // Restart Required Dialog - iOS exact message + // Restart Required Dialog if (uiState.showRestartDialog) { AlertDialog( onDismissRequest = { viewModel.dismissRestartDialog() }, @@ -712,8 +754,7 @@ private fun ApiConfigurationDialog( /** * Tool Calling Settings Section - * - * Allows users to: + * * Allows users to: * - Enable/disable tool calling * - Register demo tools (weather, time, calculator) * - Clear all registered tools @@ -859,4 +900,4 @@ fun ToolSettingsSection() { ) } } -} +} \ No newline at end of file diff --git a/examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/settings/SettingsViewModel.kt b/examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/settings/SettingsViewModel.kt index d34717f18..55f3881fe 100644 --- a/examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/settings/SettingsViewModel.kt +++ b/examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/settings/SettingsViewModel.kt @@ -34,9 +34,6 @@ data class StoredModelInfo( */ @OptIn(kotlin.time.ExperimentalTime::class) data class SettingsUiState( - // Generation Settings - val temperature: Float = 0.7f, - val maxTokens: Int = 10000, // Logging Configuration val analyticsLogToLocal: Boolean = false, // Storage Overview @@ -52,6 +49,10 @@ data class SettingsUiState( val isBaseURLConfigured: Boolean = false, val showApiConfigSheet: Boolean = false, val showRestartDialog: Boolean = false, + // Generation Settings + val temperature: Float = 0.7f, + val maxTokens: Int = 1000, + val systemPrompt: String = "", // Loading states val isLoading: Boolean = false, val errorMessage: String? = null, @@ -82,10 +83,16 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application ) } + // Preference file for general app settings (Analytics, etc) private val settingsPrefs by lazy { application.getSharedPreferences(SETTINGS_PREFS, Context.MODE_PRIVATE) } + // Preference file specifically for LLM generation parameters + private val generationPrefs by lazy { + application.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + } + companion object { private const val TAG = "SettingsViewModel" private const val ENCRYPTED_PREFS_FILE = "runanywhere_secure_prefs" @@ -93,9 +100,13 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application private const val KEY_API_KEY = "runanywhere_api_key" private const val KEY_BASE_URL = "runanywhere_base_url" private const val KEY_DEVICE_REGISTERED = "com.runanywhere.sdk.deviceRegistered" + private const val KEY_ANALYTICS_LOG_LOCAL = "analyticsLogToLocal" + + // Generation settings constants (match iOS key names) + private const val PREFS_NAME = "generation_settings" private const val KEY_TEMPERATURE = "defaultTemperature" private const val KEY_MAX_TOKENS = "defaultMaxTokens" - private const val KEY_ANALYTICS_LOG_LOCAL = "analyticsLogToLocal" + private const val KEY_SYSTEM_PROMPT = "defaultSystemPrompt" /** * Get stored API key (for use at app launch) @@ -158,37 +169,46 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application fun hasCustomConfiguration(context: Context): Boolean { return getStoredApiKey(context) != null && getStoredBaseURL(context) != null } + + /** + * Data class for generation settings + */ + data class GenerationSettings( + val temperature: Float, + val maxTokens: Int, + val systemPrompt: String? + ) + + /** + * Get generation settings (for use by ChatViewModel) + */ + fun getGenerationSettings(context: Context): GenerationSettings { + val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + val temperature = prefs.getFloat(KEY_TEMPERATURE, 0.7f) + val maxTokens = prefs.getInt(KEY_MAX_TOKENS, 1000) + val systemPrompt = prefs.getString(KEY_SYSTEM_PROMPT, "") + + return GenerationSettings( + temperature = temperature, + maxTokens = maxTokens, + systemPrompt = if (systemPrompt.isNullOrEmpty()) null else systemPrompt + ) + } } init { - loadGenerationSettings() loadAnalyticsPreference() loadApiConfiguration() + loadGenerationSettings() loadStorageData() subscribeToModelEvents() } - private fun loadGenerationSettings() { - val temp = settingsPrefs.getFloat(KEY_TEMPERATURE, 0.7f) - val max = settingsPrefs.getInt(KEY_MAX_TOKENS, 10000) - _uiState.update { it.copy(temperature = temp, maxTokens = max) } - } - private fun loadAnalyticsPreference() { val value = settingsPrefs.getBoolean(KEY_ANALYTICS_LOG_LOCAL, false) _uiState.update { it.copy(analyticsLogToLocal = value) } } - fun updateTemperature(value: Float) { - _uiState.update { it.copy(temperature = value) } - settingsPrefs.edit().putFloat(KEY_TEMPERATURE, value).apply() - } - - fun updateMaxTokens(value: Int) { - _uiState.update { it.copy(maxTokens = value) } - settingsPrefs.edit().putInt(KEY_MAX_TOKENS, value).apply() - } - fun updateAnalyticsLogToLocal(value: Boolean) { _uiState.update { it.copy(analyticsLogToLocal = value) } settingsPrefs.edit().putBoolean(KEY_ANALYTICS_LOG_LOCAL, value).apply() @@ -343,6 +363,74 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application } } + // ========== Generation Settings Management ========== + + /** + * Load generation settings from SharedPreferences + */ + private fun loadGenerationSettings() { + try { + val temperature = generationPrefs.getFloat(KEY_TEMPERATURE, 0.7f) + val maxTokens = generationPrefs.getInt(KEY_MAX_TOKENS, 1000) + val systemPrompt = generationPrefs.getString(KEY_SYSTEM_PROMPT, "") ?: "" + + _uiState.update { + it.copy( + temperature = temperature, + maxTokens = maxTokens, + systemPrompt = systemPrompt + ) + } + Log.d(TAG, "Generation settings loaded - temperature: $temperature, maxTokens: $maxTokens, systemPrompt length: ${systemPrompt.length}") + } catch (e: Exception) { + Log.e(TAG, "Failed to load generation settings", e) + } + } + + /** + * Update temperature in UI state + */ + fun updateTemperature(value: Float) { + _uiState.update { it.copy(temperature = value) } + } + + /** + * Update max tokens in UI state + */ + fun updateMaxTokens(value: Int) { + _uiState.update { it.copy(maxTokens = value) } + } + + /** + * Update system prompt in UI state + */ + fun updateSystemPrompt(value: String) { + _uiState.update { it.copy(systemPrompt = value) } + } + + /** + * Save generation settings to SharedPreferences + */ + fun saveGenerationSettings() { + viewModelScope.launch { + try { + val currentState = _uiState.value + generationPrefs.edit() + .putFloat(KEY_TEMPERATURE, currentState.temperature) + .putInt(KEY_MAX_TOKENS, currentState.maxTokens) + .putString(KEY_SYSTEM_PROMPT, currentState.systemPrompt) + .apply() + + Log.d(TAG, "Generation settings saved successfully - temperature: ${currentState.temperature}, maxTokens: ${currentState.maxTokens}") + } catch (e: Exception) { + Log.e(TAG, "Failed to save generation settings", e) + _uiState.update { + it.copy(errorMessage = "Failed to save generation settings: ${e.message}") + } + } + } + } + // ========== API Configuration Management ========== /** @@ -506,4 +594,4 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application val state = _uiState.value return state.isApiKeyConfigured && state.isBaseURLConfigured } -} +} \ No newline at end of file diff --git a/examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/tts/TextToSpeechScreen.kt b/examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/tts/TextToSpeechScreen.kt index 384529e31..87641f7af 100644 --- a/examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/tts/TextToSpeechScreen.kt +++ b/examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/tts/TextToSpeechScreen.kt @@ -482,7 +482,8 @@ private fun VoiceSettingsSection( ) } - // Pitch slider + // Pitch slider - Commented out for now + /* Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { Row( modifier = Modifier.fillMaxWidth(), @@ -510,6 +511,7 @@ private fun VoiceSettingsSection( ), ) } + */ } } } diff --git a/examples/flutter/RunAnywhereAI/lib/core/utilities/constants.dart b/examples/flutter/RunAnywhereAI/lib/core/utilities/constants.dart index 5ed4a496e..a959b8ef7 100644 --- a/examples/flutter/RunAnywhereAI/lib/core/utilities/constants.dart +++ b/examples/flutter/RunAnywhereAI/lib/core/utilities/constants.dart @@ -86,5 +86,6 @@ class PreferenceKeys { static const String routingPolicy = 'routingPolicy'; static const String defaultTemperature = 'defaultTemperature'; static const String defaultMaxTokens = 'defaultMaxTokens'; + static const String defaultSystemPrompt = 'defaultSystemPrompt'; static const String useStreaming = 'useStreaming'; } diff --git a/examples/flutter/RunAnywhereAI/lib/features/chat/chat_interface_view.dart b/examples/flutter/RunAnywhereAI/lib/features/chat/chat_interface_view.dart index 941c8ecff..e89928b2f 100644 --- a/examples/flutter/RunAnywhereAI/lib/features/chat/chat_interface_view.dart +++ b/examples/flutter/RunAnywhereAI/lib/features/chat/chat_interface_view.dart @@ -121,6 +121,11 @@ class _ChatInterfaceViewState extends State { final temperature = prefs.getDouble(PreferenceKeys.defaultTemperature) ?? 0.7; final maxTokens = prefs.getInt(PreferenceKeys.defaultMaxTokens) ?? 500; + final systemPromptRaw = + prefs.getString(PreferenceKeys.defaultSystemPrompt) ?? ''; + final systemPrompt = systemPromptRaw.isNotEmpty ? systemPromptRaw : null; + + debugPrint('[PARAMS] App _sendMessage: temperature=$temperature, maxTokens=$maxTokens, systemPrompt=${systemPrompt != null ? "set(${systemPrompt.length} chars)" : "nil"}'); // Check if tool calling is enabled and has registered tools final toolSettings = ToolSettingsViewModel.shared; @@ -134,6 +139,7 @@ class _ChatInterfaceViewState extends State { final options = sdk.LLMGenerationOptions( maxTokens: maxTokens, temperature: temperature, + systemPrompt: systemPrompt, ); if (_useStreaming) { @@ -929,4 +935,4 @@ class _MessageBubbleState extends State<_MessageBubble> { ), ); } -} +} \ No newline at end of file diff --git a/examples/flutter/RunAnywhereAI/lib/features/models/model_selection_sheet.dart b/examples/flutter/RunAnywhereAI/lib/features/models/model_selection_sheet.dart index 19e4eeb96..d578b3faa 100644 --- a/examples/flutter/RunAnywhereAI/lib/features/models/model_selection_sheet.dart +++ b/examples/flutter/RunAnywhereAI/lib/features/models/model_selection_sheet.dart @@ -502,46 +502,12 @@ class _ModelSelectionSheetState extends State { }); try { - setState(() { - _loadingProgress = 'Loading model into memory...'; - }); - - // Load model based on context/modality using real SDK - switch (widget.context) { - case ModelSelectionContext.llm: - debugPrint('🎯 Loading LLM model: ${model.id}'); - await sdk.RunAnywhere.loadModel(model.id); - break; - case ModelSelectionContext.stt: - debugPrint('🎯 Loading STT model: ${model.id}'); - await sdk.RunAnywhere.loadSTTModel(model.id); - break; - case ModelSelectionContext.tts: - debugPrint('🎯 Loading TTS voice: ${model.id}'); - await sdk.RunAnywhere.loadTTSVoice(model.id); - break; - case ModelSelectionContext.voice: - // Determine based on model category - if (model.category == ModelCategory.speechRecognition) { - debugPrint('🎯 Loading Voice STT model: ${model.id}'); - await sdk.RunAnywhere.loadSTTModel(model.id); - } else if (model.category == ModelCategory.speechSynthesis) { - debugPrint('🎯 Loading Voice TTS voice: ${model.id}'); - await sdk.RunAnywhere.loadTTSVoice(model.id); - } else { - debugPrint('🎯 Loading Voice LLM model: ${model.id}'); - await sdk.RunAnywhere.loadModel(model.id); - } - break; - } - - setState(() { - _loadingProgress = 'Model loaded successfully!'; - }); - - await Future.delayed(const Duration(milliseconds: 300)); - + // Update view model selection state await _viewModel.selectModel(model); + + // Call the callback - this is where the actual model loading happens + // The callback knows the correct context and how to load the model + debugPrint('🎯 Model selected: ${model.id}, calling callback to load'); await widget.onModelSelected(model); if (mounted) { diff --git a/examples/flutter/RunAnywhereAI/lib/features/settings/combined_settings_view.dart b/examples/flutter/RunAnywhereAI/lib/features/settings/combined_settings_view.dart index 7f34ff25d..9608002f0 100644 --- a/examples/flutter/RunAnywhereAI/lib/features/settings/combined_settings_view.dart +++ b/examples/flutter/RunAnywhereAI/lib/features/settings/combined_settings_view.dart @@ -9,6 +9,7 @@ import 'package:runanywhere_ai/core/design_system/typography.dart'; import 'package:runanywhere_ai/core/models/app_types.dart'; import 'package:runanywhere_ai/core/utilities/constants.dart'; import 'package:runanywhere_ai/core/utilities/keychain_helper.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import 'package:runanywhere_ai/features/settings/tool_settings_view_model.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -39,17 +40,31 @@ class _CombinedSettingsViewState extends State { bool _isApiKeyConfigured = false; bool _isBaseURLConfigured = false; + // Generation Settings + double _temperature = 0.7; + int _maxTokens = 1000; + String _systemPrompt = ''; + late final TextEditingController _systemPromptController; + // Loading state bool _isRefreshingStorage = false; @override void initState() { super.initState(); + _systemPromptController = TextEditingController(); unawaited(_loadSettings()); + unawaited(_loadGenerationSettings()); unawaited(_loadApiConfiguration()); unawaited(_loadStorageData()); } + @override + void dispose() { + _systemPromptController.dispose(); + super.dispose(); + } + Future _loadSettings() async { // Load from keychain _analyticsLogToLocal = @@ -59,6 +74,33 @@ class _CombinedSettingsViewState extends State { } } + /// Load generation settings from SharedPreferences + Future _loadGenerationSettings() async { + final prefs = await SharedPreferences.getInstance(); + if (mounted) { + setState(() { + _temperature = prefs.getDouble(PreferenceKeys.defaultTemperature) ?? 0.7; + _maxTokens = prefs.getInt(PreferenceKeys.defaultMaxTokens) ?? 1000; + _systemPrompt = prefs.getString(PreferenceKeys.defaultSystemPrompt) ?? ''; + _systemPromptController.text = _systemPrompt; + }); + } + } + + /// Save generation settings to SharedPreferences + Future _saveGenerationSettings() async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setDouble(PreferenceKeys.defaultTemperature, _temperature); + await prefs.setInt(PreferenceKeys.defaultMaxTokens, _maxTokens); + await prefs.setString(PreferenceKeys.defaultSystemPrompt, _systemPrompt); + + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Generation settings saved')), + ); + } + } + /// Load API configuration from keychain Future _loadApiConfiguration() async { final storedApiKey = await KeychainHelper.loadString(KeychainKeys.apiKey); @@ -393,6 +435,11 @@ class _CombinedSettingsViewState extends State { _buildApiConfigurationCard(), const SizedBox(height: AppSpacing.large), + // Generation Settings Section + _buildSectionHeader('Generation Settings'), + _buildGenerationSettingsCard(), + const SizedBox(height: AppSpacing.large), + // Storage Overview Section _buildSectionHeader('Storage Overview', trailing: _buildRefreshButton()), @@ -456,6 +503,121 @@ class _CombinedSettingsViewState extends State { ); } + Widget _buildGenerationSettingsCard() { + return Card( + child: Padding( + padding: const EdgeInsets.all(AppSpacing.large), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Temperature Slider + Text('Temperature', style: AppTypography.subheadline(context)), + const SizedBox(height: AppSpacing.xSmall), + Row( + children: [ + Expanded( + child: Slider( + value: _temperature, + min: 0.0, + max: 2.0, + divisions: 20, + label: _temperature.toStringAsFixed(1), + onChanged: (value) { + setState(() { + _temperature = value; + }); + }, + ), + ), + SizedBox( + width: 40, + child: Text( + _temperature.toStringAsFixed(1), + style: AppTypography.subheadlineSemibold(context), + textAlign: TextAlign.right, + ), + ), + ], + ), + Text( + 'Controls randomness. Lower = more focused, higher = more creative.', + style: AppTypography.caption2(context).copyWith( + color: AppColors.textSecondary(context), + ), + ), + const SizedBox(height: AppSpacing.mediumLarge), + + // Max Tokens Slider + Text('Max Tokens', style: AppTypography.subheadline(context)), + const SizedBox(height: AppSpacing.xSmall), + Row( + children: [ + Expanded( + child: Slider( + value: _maxTokens.toDouble(), + min: 50, + max: 4096, + divisions: ((4096 - 50) / 50).round(), + label: _maxTokens.toString(), + onChanged: (value) { + setState(() { + _maxTokens = value.round(); + }); + }, + ), + ), + SizedBox( + width: 60, + child: Text( + _maxTokens.toString(), + style: AppTypography.subheadlineSemibold(context), + textAlign: TextAlign.right, + ), + ), + ], + ), + Text( + 'Maximum number of tokens to generate.', + style: AppTypography.caption2(context).copyWith( + color: AppColors.textSecondary(context), + ), + ), + const SizedBox(height: AppSpacing.mediumLarge), + + // System Prompt Field + Text('System Prompt', style: AppTypography.subheadline(context)), + const SizedBox(height: AppSpacing.xSmall), + TextField( + controller: _systemPromptController, + maxLines: 3, + decoration: const InputDecoration( + hintText: 'Enter a system prompt...', + border: OutlineInputBorder(), + ), + onChanged: (value) { + _systemPrompt = value; + }, + ), + const SizedBox(height: AppSpacing.xSmall), + Text( + 'Instructions for how the model should behave.', + style: AppTypography.caption2(context).copyWith( + color: AppColors.textSecondary(context), + ), + ), + const SizedBox(height: AppSpacing.mediumLarge), + + // Save Settings Button + ElevatedButton( + onPressed: _saveGenerationSettings, + child: const Text('Save Settings'), + ), + ], + ), + ), + ); + } + Widget _buildToolCallingCard() { return ListenableBuilder( listenable: ToolSettingsViewModel.shared, @@ -1067,4 +1229,4 @@ class _ToolRow extends StatelessWidget { ), ); } -} +} \ No newline at end of file diff --git a/examples/flutter/RunAnywhereAI/lib/features/voice/text_to_speech_view.dart b/examples/flutter/RunAnywhereAI/lib/features/voice/text_to_speech_view.dart index ea6eadc20..b4d8a8c95 100644 --- a/examples/flutter/RunAnywhereAI/lib/features/voice/text_to_speech_view.dart +++ b/examples/flutter/RunAnywhereAI/lib/features/voice/text_to_speech_view.dart @@ -444,9 +444,10 @@ class _TextToSpeechViewState extends State { }); }, ), + + /* Pitch slider - Commented out for now as it is not implemented in the current TTS models. Once supported, we can have this back. const SizedBox(height: AppSpacing.mediumLarge), - // Pitch slider _buildSliderRow( label: 'Pitch', value: _pitch, @@ -459,6 +460,7 @@ class _TextToSpeechViewState extends State { }); }, ), + */ ], ), ); diff --git a/examples/ios/RunAnywhereAI/RunAnywhereAI/Features/Chat/ViewModels/LLMViewModel.swift b/examples/ios/RunAnywhereAI/RunAnywhereAI/Features/Chat/ViewModels/LLMViewModel.swift index 301bd8cf0..f68677cc0 100644 --- a/examples/ios/RunAnywhereAI/RunAnywhereAI/Features/Chat/ViewModels/LLMViewModel.swift +++ b/examples/ios/RunAnywhereAI/RunAnywhereAI/Features/Chat/ViewModels/LLMViewModel.swift @@ -324,17 +324,30 @@ final class LLMViewModel { private func getGenerationOptions() -> LLMGenerationOptions { let savedTemperature = UserDefaults.standard.double(forKey: "defaultTemperature") let savedMaxTokens = UserDefaults.standard.integer(forKey: "defaultMaxTokens") + let savedSystemPrompt = UserDefaults.standard.string(forKey: "defaultSystemPrompt") let effectiveSettings = ( temperature: savedTemperature != 0 ? savedTemperature : Self.defaultTemperatureValue, maxTokens: savedMaxTokens != 0 ? savedMaxTokens : Self.defaultMaxTokensValue ) - return LLMGenerationOptions( - maxTokens: effectiveSettings.maxTokens, - temperature: Float(effectiveSettings.temperature) - ) - } + let effectiveSystemPrompt = (savedSystemPrompt?.isEmpty == false) ? savedSystemPrompt : nil + + let systemPromptInfo: String = { + guard let prompt = effectiveSystemPrompt else { return "nil" } + return "set(\(prompt.count) chars)" + }() + + logger.info( + "[PARAMS] App getGenerationOptions: temperature=\(effectiveSettings.temperature), maxTokens=\(effectiveSettings.maxTokens), systemPrompt=\(systemPromptInfo)" + ) + + return LLMGenerationOptions( + maxTokens: effectiveSettings.maxTokens, + temperature: Float(effectiveSettings.temperature), + systemPrompt: effectiveSystemPrompt + ) +} // MARK: - Internal Methods - Helpers @@ -350,10 +363,12 @@ final class LLMViewModel { let savedMaxTokens = UserDefaults.standard.integer(forKey: "defaultMaxTokens") let maxTokens = savedMaxTokens != 0 ? savedMaxTokens : Self.defaultMaxTokensValue + let savedSystemPrompt = UserDefaults.standard.string(forKey: "defaultSystemPrompt") + UserDefaults.standard.set(temperature, forKey: "defaultTemperature") UserDefaults.standard.set(maxTokens, forKey: "defaultMaxTokens") - logger.info("Settings applied - Temperature: \(temperature), MaxTokens: \(maxTokens)") + logger.info("Settings applied - Temperature: \(temperature), MaxTokens: \(maxTokens), SystemPrompt: \(savedSystemPrompt ?? "nil")") } @objc diff --git a/examples/ios/RunAnywhereAI/RunAnywhereAI/Features/Settings/CombinedSettingsView.swift b/examples/ios/RunAnywhereAI/RunAnywhereAI/Features/Settings/CombinedSettingsView.swift index 9c1787eae..38c0e7626 100644 --- a/examples/ios/RunAnywhereAI/RunAnywhereAI/Features/Settings/CombinedSettingsView.swift +++ b/examples/ios/RunAnywhereAI/RunAnywhereAI/Features/Settings/CombinedSettingsView.swift @@ -74,6 +74,17 @@ private struct IOSSettingsContent: View { ) } + // System Prompt + Section { + TextField("Enter system prompt...", text: $viewModel.systemPrompt, axis: .vertical) + .lineLimit(3...8) + } header: { + Text("System Prompt") + } footer: { + Text("Optional instructions that define AI behavior and response style.") + .font(AppTypography.caption) + } + // Tool Calling Settings ToolSettingsSection(viewModel: toolViewModel) @@ -228,6 +239,20 @@ private struct GenerationSettingsCard: View { ) .frame(maxWidth: 200) } + + VStack(alignment: .leading, spacing: AppSpacing.smallMedium) { + HStack(alignment: .top) { + Text("System Prompt") + .frame(width: 150, alignment: .leading) + TextField("Enter system prompt...", text: $viewModel.systemPrompt, axis: .vertical) + .lineLimit(3...8) + .textFieldStyle(.plain) + .padding(AppSpacing.small) + .background(AppColors.backgroundTertiary) + .cornerRadius(AppSpacing.cornerRadiusRegular) + .frame(maxWidth: 400) + } + } } } } diff --git a/examples/ios/RunAnywhereAI/RunAnywhereAI/Features/Settings/SettingsViewModel.swift b/examples/ios/RunAnywhereAI/RunAnywhereAI/Features/Settings/SettingsViewModel.swift index 4495753bd..bf14368c9 100644 --- a/examples/ios/RunAnywhereAI/RunAnywhereAI/Features/Settings/SettingsViewModel.swift +++ b/examples/ios/RunAnywhereAI/RunAnywhereAI/Features/Settings/SettingsViewModel.swift @@ -18,6 +18,7 @@ class SettingsViewModel: ObservableObject { // Generation Settings @Published var temperature: Double = 0.7 @Published var maxTokens: Int = 10000 + @Published var systemPrompt: String = "" // API Configuration @Published var apiKey: String = "" @@ -48,6 +49,7 @@ class SettingsViewModel: ObservableObject { private let baseURLStorageKey = "runanywhere_base_url" private let temperatureDefaultsKey = "defaultTemperature" private let maxTokensDefaultsKey = "defaultMaxTokens" + private let systemPromptDefaultsKey = "defaultSystemPrompt" private let analyticsLogKey = "analyticsLogToLocal" private let deviceRegisteredKey = "com.runanywhere.sdk.deviceRegistered" @@ -113,6 +115,15 @@ class SettingsViewModel: ObservableObject { } .store(in: &cancellables) + // Auto-save system prompt changes + $systemPrompt + .debounce(for: 0.5, scheduler: DispatchQueue.main) + .dropFirst() // Skip initial value to avoid saving on init + .sink { [weak self] newValue in + self?.saveSystemPrompt(newValue) + } + .store(in: &cancellables) + // Auto-save analytics logging preference $analyticsLogToLocal .dropFirst() // Skip initial value to avoid saving on init @@ -139,6 +150,9 @@ class SettingsViewModel: ObservableObject { // Load max tokens let savedMaxTokens = UserDefaults.standard.integer(forKey: maxTokensDefaultsKey) maxTokens = savedMaxTokens > 0 ? savedMaxTokens : 10000 + + // Load system prompt + systemPrompt = UserDefaults.standard.string(forKey: systemPromptDefaultsKey) ?? "" } private func loadApiKeyConfiguration() { @@ -181,11 +195,17 @@ class SettingsViewModel: ObservableObject { print("Settings: Saved max tokens: \(value)") } + private func saveSystemPrompt(_ value: String) { + UserDefaults.standard.set(value, forKey: systemPromptDefaultsKey) + print("Settings: Saved system prompt (\(value.count) chars)") + } + /// Get current generation configuration for SDK usage func getGenerationConfiguration() -> GenerationConfiguration { GenerationConfiguration( temperature: temperature, - maxTokens: maxTokens + maxTokens: maxTokens, + systemPrompt: systemPrompt.isEmpty ? nil : systemPrompt ) } @@ -397,4 +417,5 @@ class SettingsViewModel: ObservableObject { struct GenerationConfiguration { let temperature: Double let maxTokens: Int + let systemPrompt: String? } diff --git a/examples/ios/RunAnywhereAI/RunAnywhereAI/Features/Voice/TTSViewModel.swift b/examples/ios/RunAnywhereAI/RunAnywhereAI/Features/Voice/TTSViewModel.swift index 4c9a1fe93..16fe78dba 100644 --- a/examples/ios/RunAnywhereAI/RunAnywhereAI/Features/Voice/TTSViewModel.swift +++ b/examples/ios/RunAnywhereAI/RunAnywhereAI/Features/Voice/TTSViewModel.swift @@ -26,7 +26,7 @@ class TTSViewModel: ObservableObject { // Voice Settings @Published var speechRate: Double = 1.0 - @Published var pitch: Double = 1.0 + @Published var pitch: Double = 1.0 // while removed from the UI, the backend still supports pitch, so maintaining it here. // MARK: - Private Properties diff --git a/examples/ios/RunAnywhereAI/RunAnywhereAI/Features/Voice/TextToSpeechView.swift b/examples/ios/RunAnywhereAI/RunAnywhereAI/Features/Voice/TextToSpeechView.swift index ff178564b..d47e5f30b 100644 --- a/examples/ios/RunAnywhereAI/RunAnywhereAI/Features/Voice/TextToSpeechView.swift +++ b/examples/ios/RunAnywhereAI/RunAnywhereAI/Features/Voice/TextToSpeechView.swift @@ -233,21 +233,23 @@ struct TextToSpeechView: View { Slider(value: $viewModel.speechRate, in: 0.5...2.0, step: 0.1) .tint(AppColors.primaryAccent) } - - // Pitch - VStack(alignment: .leading, spacing: 10) { - HStack { - Text("Pitch") - .font(.subheadline) - .foregroundColor(.secondary) - Spacer() - Text(String(format: "%.1fx", viewModel.pitch)) - .font(.system(size: 15, weight: .medium, design: .rounded)) - .foregroundColor(.primary) - } - Slider(value: $viewModel.pitch, in: 0.5...2.0, step: 0.1) - .tint(AppColors.primaryPurple) - } + + // TODO: Find a model for TTS that supports pitch, or manually implement a good quality pitch adjustment + + // Pitch (not implemented in the current TTS models. Once supported, we can have this back.) + // VStack(alignment: .leading, spacing: 10) { + // HStack { + // Text("Pitch") + // .font(.subheadline) + // .foregroundColor(.secondary) + // Spacer() + // Text(String(format: "%.1fx", viewModel.pitch)) + // .font(.system(size: 15, weight: .medium, design: .rounded)) + // .foregroundColor(.primary) + // } + // Slider(value: $viewModel.pitch, in: 0.5...2.0, step: 0.1) + // .tint(AppColors.primaryPurple) + // } } .padding(20) .background(AppColors.backgroundTertiary) diff --git a/examples/react-native/RunAnywhereAI/src/screens/ChatScreen.tsx b/examples/react-native/RunAnywhereAI/src/screens/ChatScreen.tsx index 55a134a36..e9dfab79a 100644 --- a/examples/react-native/RunAnywhereAI/src/screens/ChatScreen.tsx +++ b/examples/react-native/RunAnywhereAI/src/screens/ChatScreen.tsx @@ -31,6 +31,7 @@ import { Alert, Modal, } from 'react-native'; +import AsyncStorage from '@react-native-async-storage/async-storage'; import Icon from 'react-native-vector-icons/Ionicons'; import { Colors } from '../theme/colors'; import { Typography } from '../theme/typography'; @@ -48,9 +49,10 @@ import { ModelSelectionSheet, ModelSelectionContext, } from '../components/model'; +import { GENERATION_SETTINGS_KEYS } from '../types/settings'; // Import RunAnywhere SDK (Multi-Package Architecture) -import { RunAnywhere, type ModelInfo as SDKModelInfo } from '@runanywhere/core'; +import { RunAnywhere, type ModelInfo as SDKModelInfo, type GenerationOptions } from '@runanywhere/core'; import { safeEvaluateExpression } from '../utils/mathParser'; // Generate unique ID @@ -172,8 +174,7 @@ const registerChatTools = () => { /** * Detect tool call format based on model ID and name * LFM2-Tool models use Pythonic format, others use JSON format - * - * Matches iOS: LLMViewModel+ToolCalling.swift detectToolCallFormat() + * * Matches iOS: LLMViewModel+ToolCalling.swift detectToolCallFormat() * Checks both ID and name since model might be identified by either */ const detectToolCallFormat = (modelId: string | undefined, modelName: string | undefined): string => { @@ -262,6 +263,24 @@ export const ChatScreen: React.FC = () => { // Messages from current conversation const messages = currentConversation?.messages || []; + /** + * Get generation options from AsyncStorage + * Reads user-configured temperature, maxTokens, and systemPrompt + */ + const getGenerationOptions = async (): Promise => { + const tempStr = await AsyncStorage.getItem(GENERATION_SETTINGS_KEYS.TEMPERATURE); + const maxStr = await AsyncStorage.getItem(GENERATION_SETTINGS_KEYS.MAX_TOKENS); + const sysStr = await AsyncStorage.getItem(GENERATION_SETTINGS_KEYS.SYSTEM_PROMPT); + + const temperature = tempStr !== null && !Number.isNaN(parseFloat(tempStr)) ? parseFloat(tempStr) : 0.7; + const maxTokens = maxStr ? parseInt(maxStr, 10) : 1000; + const systemPrompt = sysStr && sysStr.trim() !== '' ? sysStr : undefined; + + console.log(`[PARAMS] App getGenerationOptions: temperature=${temperature}, maxTokens=${maxTokens}, systemPrompt=${systemPrompt ? `set(${systemPrompt.length} chars)` : 'nil'}`); + + return { temperature, maxTokens, systemPrompt }; + }; + /** * Load available LLM models from catalog */ @@ -435,13 +454,17 @@ export const ChatScreen: React.FC = () => { const format = detectToolCallFormat(currentModel?.id, currentModel?.name); console.log('[ChatScreen] Starting generation with tools for:', prompt, 'model:', currentModel?.id, 'format:', format); + // Get user-configured generation options + const options = await getGenerationOptions(); + // Use tool-enabled generation // If the LLM needs to call a tool (like weather API), it happens automatically const result = await RunAnywhere.generateWithTools(prompt, { autoExecute: true, maxToolCalls: 3, - maxTokens: 1000, - temperature: 0.7, + maxTokens: options.maxTokens, + temperature: options.temperature, + systemPrompt: options.systemPrompt, format: format, }); diff --git a/examples/react-native/RunAnywhereAI/src/screens/SettingsScreen.tsx b/examples/react-native/RunAnywhereAI/src/screens/SettingsScreen.tsx index cec9e71a0..ddb506476 100644 --- a/examples/react-native/RunAnywhereAI/src/screens/SettingsScreen.tsx +++ b/examples/react-native/RunAnywhereAI/src/screens/SettingsScreen.tsx @@ -1,37 +1,37 @@ /** - * SettingsScreen - Tab 4: Settings & Storage - * - * Provides SDK configuration, model management, and storage overview. - * Matches iOS CombinedSettingsView architecture and patterns. - * - * Features: - * - Generation settings (temperature, max tokens) - * - API configuration - * - Storage overview (total usage, available space, models storage) - * - Downloaded models list with delete functionality - * - Storage management (clear cache, clean temp files) - * - SDK info (version, capabilities, loaded models) - * - * Architecture: - * - Fetches SDK state via RunAnywhere methods - * - Shows available vs downloaded models - * - Manages model downloads and deletions - * - Displays backend info and capabilities - * - * Reference: iOS examples/ios/RunAnywhereAI/RunAnywhereAI/Features/Settings/CombinedSettingsView.swift - */ + * SettingsScreen - Tab 4: Settings & Storage + * + * Provides SDK configuration, model management, and storage overview. + * Matches iOS CombinedSettingsView architecture and patterns. + * + * Features: + * - Generation settings (temperature, max tokens) + * - API configuration + * - Storage overview (total usage, available space, models storage) + * - Downloaded models list with delete functionality + * - Storage management (clear cache, clean temp files) + * - SDK info (version, capabilities, loaded models) + * + * Architecture: + * - Fetches SDK state via RunAnywhere methods + * - Shows available vs downloaded models + * - Manages model downloads and deletions + * - Displays backend info and capabilities + * + * Reference: iOS examples/ios/RunAnywhereAI/RunAnywhereAI/Features/Settings/CombinedSettingsView.swift + */ import React, { useState, useCallback, useEffect } from 'react'; import { - View, - Text, - StyleSheet, - SafeAreaView, - ScrollView, - TouchableOpacity, - Alert, - TextInput, - Modal, +  View, +  Text, +  StyleSheet, +  SafeAreaView, +  ScrollView, +  TouchableOpacity, +  Alert, +  TextInput, +  Modal, } from 'react-native'; import AsyncStorage from '@react-native-async-storage/async-storage'; import Icon from 'react-native-vector-icons/Ionicons'; @@ -40,9 +40,10 @@ import { Typography } from '../theme/typography'; import { Spacing, Padding, BorderRadius } from '../theme/spacing'; import type { StorageInfo } from '../types/settings'; import { - RoutingPolicy, - RoutingPolicyDisplayNames, - SETTINGS_CONSTRAINTS, +  RoutingPolicy, +  RoutingPolicyDisplayNames, +  SETTINGS_CONSTRAINTS, +  GENERATION_SETTINGS_KEYS, } from '../types/settings'; import { LLMFramework, FrameworkDisplayNames } from '../types/model'; import { safeEvaluateExpression } from '../utils/mathParser'; @@ -52,1839 +53,1947 @@ import { RunAnywhere, type ModelInfo } from '@runanywhere/core'; // Storage keys for API configuration const STORAGE_KEYS = { - API_KEY: '@runanywhere_api_key', - BASE_URL: '@runanywhere_base_url', - DEVICE_REGISTERED: '@runanywhere_device_registered', - TOOL_CALLING_ENABLED: '@runanywhere_tool_calling_enabled', +  API_KEY: '@runanywhere_api_key', +  BASE_URL: '@runanywhere_base_url', +  DEVICE_REGISTERED: '@runanywhere_device_registered', +  TOOL_CALLING_ENABLED: '@runanywhere_tool_calling_enabled', }; /** - * Get stored API key (for use at app launch) - */ + * Get stored API key (for use at app launch) + */ export const getStoredApiKey = async (): Promise => { - try { - return await AsyncStorage.getItem(STORAGE_KEYS.API_KEY); - } catch { - return null; - } +  try { +    return await AsyncStorage.getItem(STORAGE_KEYS.API_KEY); +  } catch { +    return null; +  } }; /** - * Get stored base URL (for use at app launch) - * Automatically adds https:// if no scheme is present - */ + * Get stored base URL (for use at app launch) + * Automatically adds https:// if no scheme is present + */ export const getStoredBaseURL = async (): Promise => { - try { - const value = await AsyncStorage.getItem(STORAGE_KEYS.BASE_URL); - if (!value) return null; - const trimmed = value.trim(); - if (trimmed.startsWith('http://') || trimmed.startsWith('https://')) { - return trimmed; - } - return `https://${trimmed}`; - } catch { - return null; - } +  try { +    const value = await AsyncStorage.getItem(STORAGE_KEYS.BASE_URL); +    if (!value) return null; +    const trimmed = value.trim(); +    if (trimmed.startsWith('http://') || trimmed.startsWith('https://')) { +      return trimmed; +    } +    return `https://${trimmed}`; +  } catch { +    return null; +  } }; /** - * Check if custom configuration is set - */ + * Check if custom configuration is set + */ export const hasCustomConfiguration = async (): Promise => { - const apiKey = await getStoredApiKey(); - const baseURL = await getStoredBaseURL(); - return apiKey !== null && baseURL !== null && apiKey !== '' && baseURL !== ''; +  const apiKey = await getStoredApiKey(); +  const baseURL = await getStoredBaseURL(); +  return apiKey !== null && baseURL !== null && apiKey !== '' && baseURL !== ''; }; // Default storage info const DEFAULT_STORAGE_INFO: StorageInfo = { - totalStorage: 256 * 1024 * 1024 * 1024, - appStorage: 0, - modelsStorage: 0, - cacheSize: 0, - freeSpace: 100 * 1024 * 1024 * 1024, +  totalStorage: 256 * 1024 * 1024 * 1024, +  appStorage: 0, +  modelsStorage: 0, +  cacheSize: 0, +  freeSpace: 100 * 1024 * 1024 * 1024, }; /** - * Format bytes to human readable - */ + * Format bytes to human readable + */ const formatBytes = (bytes: number): string => { - if (bytes === 0) return '0 B'; - const k = 1024; - const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; - const i = Math.floor(Math.log(bytes) / Math.log(k)); - return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`; +  if (bytes === 0) return '0 B'; +  const k = 1024; +  const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; +  const i = Math.floor(Math.log(bytes) / Math.log(k)); +  return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`; }; export const SettingsScreen: React.FC = () => { - // Settings state - const [routingPolicy, setRoutingPolicy] = useState( - RoutingPolicy.Automatic - ); - const [temperature, setTemperature] = useState(0.7); - const [maxTokens, setMaxTokens] = useState(10000); - const [apiKeyConfigured, setApiKeyConfigured] = useState(false); - - // API Configuration state - const [apiKey, setApiKey] = useState(''); - const [baseURL, setBaseURL] = useState(''); - const [isBaseURLConfigured, setIsBaseURLConfigured] = useState(false); - const [showApiConfigModal, setShowApiConfigModal] = useState(false); - const [showPassword, setShowPassword] = useState(false); - - // Storage state - const [storageInfo, setStorageInfo] = - useState(DEFAULT_STORAGE_INFO); - const [_isRefreshing, setIsRefreshing] = useState(false); - const [sdkVersion, setSdkVersion] = useState('0.1.0'); - - // SDK State - const [capabilities, setCapabilities] = useState([]); - const [backendInfoData, setBackendInfoData] = useState< - Record - >({}); - const [isSTTLoaded, setIsSTTLoaded] = useState(false); - const [isTTSLoaded, setIsTTSLoaded] = useState(false); - const [isTextLoaded, setIsTextLoaded] = useState(false); - const [isVADLoaded, setIsVADLoaded] = useState(false); - const [_memoryUsage, _setMemoryUsage] = useState(0); - - // Model catalog state - const [availableModels, setAvailableModels] = useState([]); - const [downloadingModels, setDownloadingModels] = useState< - Record - >({}); - const [downloadedModels, setDownloadedModels] = useState([]); - - // Tool calling state - const [toolCallingEnabled, setToolCallingEnabled] = useState(false); - const [registeredTools, setRegisteredTools] = useState}>>([]); - - // Capability names mapping - const capabilityNames: Record = { - 0: 'STT (Speech-to-Text)', - 1: 'TTS (Text-to-Speech)', - 2: 'Text Generation', - 3: 'Embeddings', - 4: 'VAD (Voice Activity)', - 5: 'Diarization', - }; - - // Load data on mount - useEffect(() => { - loadData(); - loadApiConfiguration(); - loadToolCallingSettings(); - }, []); - - /** - * Load API configuration from AsyncStorage - */ - const loadApiConfiguration = async () => { - try { - const storedApiKey = await AsyncStorage.getItem(STORAGE_KEYS.API_KEY); - const storedBaseURL = await AsyncStorage.getItem(STORAGE_KEYS.BASE_URL); - - setApiKey(storedApiKey || ''); - setBaseURL(storedBaseURL || ''); - setApiKeyConfigured(!!storedApiKey && storedApiKey !== ''); - setIsBaseURLConfigured(!!storedBaseURL && storedBaseURL !== ''); - } catch (error) { - console.error('[Settings] Failed to load API configuration:', error); - } - }; - - /** - * Load tool calling settings from AsyncStorage - */ - const loadToolCallingSettings = async () => { - try { - const enabled = await AsyncStorage.getItem(STORAGE_KEYS.TOOL_CALLING_ENABLED); - setToolCallingEnabled(enabled === 'true'); - refreshRegisteredTools(); - } catch (error) { - console.error('[Settings] Failed to load tool calling settings:', error); - } - }; - - /** - * Refresh the list of registered tools from SDK - */ - const refreshRegisteredTools = () => { - const tools = RunAnywhere.getRegisteredTools(); - setRegisteredTools(tools.map(t => ({ - name: t.name, - description: t.description, - parameters: t.parameters || [], - }))); - }; - - /** - * Toggle tool calling enabled state - */ - const handleToggleToolCalling = async (enabled: boolean) => { - setToolCallingEnabled(enabled); - try { - await AsyncStorage.setItem(STORAGE_KEYS.TOOL_CALLING_ENABLED, enabled ? 'true' : 'false'); - } catch (error) { - console.error('[Settings] Failed to save tool calling setting:', error); - } - }; - - /** - * Register demo tools (weather, time, calculator) - */ - const registerDemoTools = () => { - // Clear existing tools - RunAnywhere.clearTools(); - - // Weather tool - Real API (wttr.in - no key needed) - RunAnywhere.registerTool( - { - name: 'get_weather', - description: 'Gets the current weather for a city or location', - parameters: [ - { - name: 'location', - type: 'string', - description: 'City name or location (e.g., "Tokyo", "New York", "London")', - required: true, - }, - ], - }, - async (args: Record) => { - const location = (args.location as string) || 'San Francisco'; - try { - const response = await fetch( - `https://wttr.in/${encodeURIComponent(location)}?format=j1` - ); - const data = await response.json(); - const current = data.current_condition?.[0]; - return { - location, - temperature_c: current?.temp_C || 'N/A', - temperature_f: current?.temp_F || 'N/A', - condition: current?.weatherDesc?.[0]?.value || 'Unknown', - humidity: current?.humidity || 'N/A', - wind_kph: current?.windspeedKmph || 'N/A', - }; - } catch (error) { - return { error: `Failed to get weather: ${error}` }; - } - } - ); - - // Time tool - Real system time - RunAnywhere.registerTool( - { - name: 'get_current_time', - description: 'Gets the current date, time, and timezone information', - parameters: [], - }, - async () => { - const now = new Date(); - return { - datetime: now.toLocaleString(), - time: now.toLocaleTimeString(), - timestamp: now.toISOString(), - timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, - }; - } - ); - - // Calculator tool - Math evaluation - RunAnywhere.registerTool( - { - name: 'calculate', - description: 'Performs math calculations. Supports +, -, *, /, and parentheses', - parameters: [ - { - name: 'expression', - type: 'string', - description: 'Math expression (e.g., "2 + 2 * 3", "(10 + 5) / 3")', - required: true, - }, - ], - }, - async (args: Record) => { - const expression = (args.expression as string) || '0'; - try { - // Safe math evaluation using recursive descent parser - const result = safeEvaluateExpression(expression); - return { - expression: expression, - result: result, - }; - } catch (error) { - return { error: `Failed to calculate: ${error}` }; - } - } - ); - - refreshRegisteredTools(); - Alert.alert('Demo Tools Added', '3 demo tools have been registered: get_weather, get_current_time, calculate'); - }; - - /** - * Clear all registered tools - */ - const clearAllTools = () => { - Alert.alert( - 'Clear All Tools', - 'Are you sure you want to remove all registered tools?', - [ - { text: 'Cancel', style: 'cancel' }, - { - text: 'Clear', - style: 'destructive', - onPress: () => { - RunAnywhere.clearTools(); - refreshRegisteredTools(); - }, - }, - ] - ); - }; - - /** - * Normalize base URL by adding https:// if no scheme is present - */ - const normalizeBaseURL = (url: string): string => { - const trimmed = url.trim(); - if (!trimmed) return trimmed; - if (trimmed.startsWith('http://') || trimmed.startsWith('https://')) { - return trimmed; - } - return `https://${trimmed}`; - }; - - /** - * Save API configuration to AsyncStorage - */ - const saveApiConfiguration = async () => { - try { - const normalizedURL = normalizeBaseURL(baseURL); - await AsyncStorage.setItem(STORAGE_KEYS.API_KEY, apiKey); - await AsyncStorage.setItem(STORAGE_KEYS.BASE_URL, normalizedURL); - - setBaseURL(normalizedURL); - setApiKeyConfigured(!!apiKey); - setIsBaseURLConfigured(!!normalizedURL); - setShowApiConfigModal(false); - - Alert.alert( - 'Restart Required', - 'API configuration has been updated. Please restart the app for changes to take effect.', - [{ text: 'OK' }] - ); - } catch (error) { - Alert.alert('Error', `Failed to save API configuration: ${error}`); - } - }; - - /** - * Clear API configuration from AsyncStorage - */ - const clearApiConfiguration = async () => { - try { - await AsyncStorage.multiRemove([ - STORAGE_KEYS.API_KEY, - STORAGE_KEYS.BASE_URL, - STORAGE_KEYS.DEVICE_REGISTERED, - ]); - - setApiKey(''); - setBaseURL(''); - setApiKeyConfigured(false); - setIsBaseURLConfigured(false); - - Alert.alert( - 'Restart Required', - 'API configuration has been cleared. Please restart the app for changes to take effect.', - [{ text: 'OK' }] - ); - } catch (error) { - Alert.alert('Error', `Failed to clear API configuration: ${error}`); - } - }; - - const loadData = async () => { - setIsRefreshing(true); - try { - // Get SDK version - const version = await RunAnywhere.getVersion(); - setSdkVersion(version); - - // Check if SDK is initialized first - const isInit = await RunAnywhere.isInitialized(); - console.log('[Settings] SDK isInitialized:', isInit); - - // Get backend info for storage data - const backendInfo = await RunAnywhere.getBackendInfo(); - console.log('[Settings] Backend info:', backendInfo); - - // Override name with actual init status - const updatedBackendInfo = { - ...backendInfo, - name: isInit ? 'RunAnywhere Core' : 'Not initialized', - version: version, - initialized: isInit, - }; - setBackendInfoData(updatedBackendInfo); - - // Get capabilities (returns string[], not number[]) - const caps = await RunAnywhere.getCapabilities(); - console.warn('[Settings] Capabilities:', caps); - // Convert string capabilities to numbers for display mapping - const capNumbers = caps.map((cap, index) => index); - setCapabilities(capNumbers); - - // Check loaded models - const sttLoaded = await RunAnywhere.isSTTModelLoaded(); - const ttsLoaded = await RunAnywhere.isTTSModelLoaded(); - const textLoaded = await RunAnywhere.isModelLoaded(); - const vadLoaded = await RunAnywhere.isVADModelLoaded(); - - setIsSTTLoaded(sttLoaded); - setIsTTSLoaded(ttsLoaded); - setIsTextLoaded(textLoaded); - setIsVADLoaded(vadLoaded); - - console.warn( - '[Settings] Models loaded - STT:', - sttLoaded, - 'TTS:', - ttsLoaded, - 'Text:', - textLoaded, - 'VAD:', - vadLoaded - ); - - // Get available models from catalog - try { - const available = await RunAnywhere.getAvailableModels(); - console.warn('[Settings] Available models:', available); - setAvailableModels(available); - } catch (err) { - console.warn('[Settings] Failed to get available models:', err); - } - - // Get downloaded models - try { - const downloaded = await RunAnywhere.getDownloadedModels(); - console.warn('[Settings] Downloaded models:', downloaded); - setDownloadedModels(downloaded); - } catch (err) { - console.warn('[Settings] Failed to get downloaded models:', err); - } - - // Get storage info using new SDK API - try { - const storage = await RunAnywhere.getStorageInfo(); - console.warn('[Settings] Storage info:', storage); - setStorageInfo({ - totalStorage: storage.deviceStorage.totalSpace, - appStorage: storage.appStorage.totalSize, - modelsStorage: storage.modelStorage.totalSize, - cacheSize: storage.cacheSize, - freeSpace: storage.deviceStorage.freeSpace, - }); - } catch (err) { - console.warn('[Settings] Failed to get storage info:', err); - } - } catch (error) { - console.error('Failed to load data:', error); - } finally { - setIsRefreshing(false); - } - }; - - /** - * Handle routing policy change - */ - const handleRoutingPolicyChange = useCallback(() => { - const policies = Object.values(RoutingPolicy); - Alert.alert( - 'Routing Policy', - 'Choose how requests are routed', - policies.map((policy) => ({ - text: RoutingPolicyDisplayNames[policy], - onPress: () => { - setRoutingPolicy(policy); - }, - })) - ); - }, []); - - /** - * Handle API key configuration - open modal - */ - const handleConfigureApiKey = useCallback(() => { - setShowApiConfigModal(true); - }, []); - - /** - * Cancel API configuration modal - */ - const handleCancelApiConfig = useCallback(() => { - loadApiConfiguration(); // Reset to stored values - setShowApiConfigModal(false); - }, []); - - - - /** - * Handle clear cache - */ - const handleClearCache = useCallback(() => { - Alert.alert( - 'Clear Cache', - 'This will clear temporary files. Models will not be deleted.', - [ - { text: 'Cancel', style: 'cancel' }, - { - text: 'Clear', - style: 'destructive', - onPress: async () => { - try { - // Clear SDK cache using new Storage API - await RunAnywhere.clearCache(); - await RunAnywhere.cleanTempFiles(); - Alert.alert('Success', 'Cache cleared successfully'); - loadData(); - } catch (err) { - console.error('[Settings] Failed to clear cache:', err); - Alert.alert('Error', `Failed to clear cache: ${err}`); - } - }, - }, - ] - ); - }, []); - - /** - * Handle model download - */ - const handleDownloadModel = useCallback( - async (model: ModelInfo) => { - if (downloadingModels[model.id] !== undefined) { - // Already downloading, cancel it - try { - await RunAnywhere.cancelDownload(model.id); - setDownloadingModels((prev) => { - const updated = { ...prev }; - delete updated[model.id]; - return updated; - }); - } catch (err) { - console.error('Failed to cancel download:', err); - } - return; - } - - // Start download with progress tracking - setDownloadingModels((prev) => ({ ...prev, [model.id]: 0 })); - - try { - await RunAnywhere.downloadModel(model.id, (progress) => { - console.warn( - `[Settings] Download progress for ${model.id}: ${(progress.progress * 100).toFixed(1)}%` - ); - setDownloadingModels((prev) => ({ - ...prev, - [model.id]: progress.progress, - })); - }); - - // Download complete - setDownloadingModels((prev) => { - const updated = { ...prev }; - delete updated[model.id]; - return updated; - }); - - Alert.alert('Success', `${model.name} downloaded successfully!`); - loadData(); // Refresh to show downloaded model - } catch (err) { - setDownloadingModels((prev) => { - const updated = { ...prev }; - delete updated[model.id]; - return updated; - }); - Alert.alert( - 'Download Failed', - `Failed to download ${model.name}: ${err}` - ); - } - }, - [downloadingModels] - ); - - /** - * Handle delete downloaded model - */ - const handleDeleteDownloadedModel = useCallback(async (model: ModelInfo) => { - const downloadedModel = downloadedModels.find((m) => m.id === model.id); - // Prefer downloaded model's size (actual disk usage) over catalog downloadSize (expected size) - // TODO: Replace with actual disk size once SDK exposes it (e.g., sizeOnDisk or actualSize) - const freedSize = - downloadedModel?.downloadSize ?? // Use downloaded model's size when available - model.downloadSize ?? - 0; - - Alert.alert( - 'Delete Model', - `Are you sure you want to delete ${model.name}? This will free up ${formatBytes(freedSize)}.`, - [ - { text: 'Cancel', style: 'cancel' }, - { - text: 'Delete', - style: 'destructive', - onPress: async () => { - try { - await RunAnywhere.deleteModel(model.id); - Alert.alert('Deleted', `${model.name} has been deleted.`); - loadData(); // Refresh list - } catch (err) { - Alert.alert('Error', `Failed to delete: ${err}`); - } - }, - }, - ] - ); - }, [downloadedModels]); - - /** - * Handle clear all data - */ - const handleClearAllData = useCallback(() => { - Alert.alert( - 'Clear All Data', - 'This will delete all models and reset the app. This action cannot be undone.', - [ - { text: 'Cancel', style: 'cancel' }, - { - text: 'Clear All', - style: 'destructive', - onPress: async () => { - try { - // Unload all models - await RunAnywhere.unloadModel(); - await RunAnywhere.unloadSTTModel(); - await RunAnywhere.unloadTTSModel(); - // Destroy SDK - await RunAnywhere.destroy(); - Alert.alert('Success', 'All data cleared'); - } catch (error) { - Alert.alert('Error', `Failed to clear data: ${error}`); - } - loadData(); - }, - }, - ] - ); - }, []); - - /** - * Render section header - */ - const renderSectionHeader = (title: string) => ( - {title} - ); - - /** - * Render setting row - */ - const renderSettingRow = ( - icon: string, - title: string, - value: string, - onPress?: () => void, - showChevron: boolean = true - ) => ( - - - - {title} - - - {value} - {showChevron && onPress && ( - - )} - - - ); - - /** - * Render slider setting - */ - const renderSliderSetting = ( - title: string, - value: number, - onChange: (value: number) => void, - min: number, - max: number, - step: number, - formatValue: (v: number) => string - ) => ( - - - {title} - {formatValue(value)} - - - onChange(Math.max(min, value - step))} - > - - - - - - onChange(Math.min(max, value + step))} - > - - - - - ); - - /** - * Render storage bar - * Matches iOS: shows app storage usage relative to device free space - */ - const renderStorageBar = () => { - // Show app storage as portion of (app storage + free space) - const totalAvailable = storageInfo.appStorage + storageInfo.freeSpace; - const usedPercent = totalAvailable > 0 - ? (storageInfo.appStorage / totalAvailable) * 100 - : 0; - return ( - - - - - - {formatBytes(storageInfo.appStorage)} of{' '} - {formatBytes(storageInfo.freeSpace)} available - - - ); - }; - - - - /** - * Render catalog model row - */ - const renderCatalogModelRow = (model: ModelInfo) => { - const isDownloading = downloadingModels[model.id] !== undefined; - const downloadProgress = downloadingModels[model.id] || 0; - const isDownloaded = downloadedModels.some((m) => m.id === model.id); - - // Determine framework based on format - const framework = model.format === 'onnx' ? LLMFramework.ONNX : LLMFramework.LlamaCpp; - const frameworkName = FrameworkDisplayNames[framework] || framework; - - // Get model size estimate based on download size (may differ from actual on-disk size) - const downloadedModel = downloadedModels.find((m) => m.id === model.id); - const modelSize = - downloadedModel?.downloadSize ?? // Prefer size from downloaded model when available - model.downloadSize ?? // Fall back to catalog's expected download size - 0; - return ( - - - - {model.name} - - {model.category} - - - {model.metadata?.description && ( - - {model.metadata.description} - - )} - - - {formatBytes(modelSize)} - - {frameworkName} - - {isDownloading && ( - - - - - - {(downloadProgress * 100).toFixed(0)}% - - - )} - - - isDownloaded - ? handleDeleteDownloadedModel(model) - : handleDownloadModel(model) - } - > - - - - ); - }; - - return ( - - {/* Header */} - - Settings - - - - - - - {/* Generation Settings - Matches iOS CombinedSettingsView order */} - {renderSectionHeader('Generation Settings')} - - {renderSliderSetting( - 'Temperature', - temperature, - setTemperature, - SETTINGS_CONSTRAINTS.temperature.min, - SETTINGS_CONSTRAINTS.temperature.max, - SETTINGS_CONSTRAINTS.temperature.step, - (v) => v.toFixed(1) - )} - {renderSliderSetting( - 'Max Tokens', - maxTokens, - setMaxTokens, - SETTINGS_CONSTRAINTS.maxTokens.min, - SETTINGS_CONSTRAINTS.maxTokens.max, - SETTINGS_CONSTRAINTS.maxTokens.step, - (v) => v.toLocaleString() - )} - - - {/* API Configuration (Testing) */} - {renderSectionHeader('API Configuration (Testing)')} - - - API Key - - {apiKeyConfigured ? 'Configured' : 'Not Set'} - - - - - Base URL - - {isBaseURLConfigured ? 'Configured' : 'Not Set'} - - - - - - Configure - - {apiKeyConfigured && isBaseURLConfigured && ( - - Clear - - )} - - - Configure custom API key and base URL for testing. Requires app restart. - - - - {/* Tool Settings - Matches iOS ToolSettingsView */} - {renderSectionHeader('Tool Settings')} - - {/* Enable Tool Calling Toggle */} - - - Enable Tool Calling - - Allow LLMs to call tools (APIs, functions) - - - handleToggleToolCalling(!toolCallingEnabled)} - > - - - - - {toolCallingEnabled && ( - <> - - - {/* Registered Tools Count */} - - Registered Tools - 0 ? Colors.primaryGreen : Colors.textSecondary }]}> - {registeredTools.length} {registeredTools.length === 1 ? 'tool' : 'tools'} - - - - {/* Demo Tools Button */} - {registeredTools.length === 0 && ( - <> - - - - Add Demo Tools - - - )} - - {/* Registered Tools List */} - {registeredTools.length > 0 && ( - <> - - {registeredTools.map((tool, index) => ( - - - - {tool.name} - {tool.description} - {tool.parameters.length > 0 && ( - - {tool.parameters.map((p) => ( - - {p.name} - - ))} - - )} - - {index < registeredTools.length - 1 && } - - ))} - - {/* Clear All Tools Button */} - - - - Clear All Tools - - - )} - - )} - - - Tools allow the LLM to call external APIs and functions to get real-time data. - - - - {/* Storage Overview - Matches iOS CombinedSettingsView */} - {renderSectionHeader('Storage Overview')} - - {renderStorageBar()} - - {/* Total Storage - App's total storage usage */} - - Total Storage - - {formatBytes(storageInfo.appStorage)} - - - {/* Models Storage - Downloaded models size */} - - Models - - {formatBytes(storageInfo.modelsStorage)} - - - {/* Cache Size */} - - Cache - - {formatBytes(storageInfo.cacheSize)} - - - {/* Available - Device free space */} - - Available - - {formatBytes(storageInfo.freeSpace)} - - - - - - {/* Model Catalog */} - {renderSectionHeader('Model Catalog')} - - {availableModels.length === 0 ? ( - Loading models... - ) : ( - availableModels.map(renderCatalogModelRow) - )} - - - {/* Storage Management */} - {renderSectionHeader('Storage Management')} - - - - Clear Cache - - - - - Clear All Data - - - - - {/* Version Info */} - - RunAnywhere AI - SDK v{sdkVersion} - - - - {/* API Configuration Modal */} - - - - API Configuration - - {/* API Key Input */} - - API Key - - - setShowPassword(!showPassword)} - > - - - - - Your API key for authenticating with the backend - - - - {/* Base URL Input */} - - Base URL - - - The backend API URL (https:// added automatically if missing) - - - - {/* Warning */} - - - - After saving, you must restart the app for changes to take effect. The SDK will reinitialize with your custom configuration. - - - - {/* Buttons */} - - - Cancel - - - Save - - - - - - - ); +  // Settings state +  const [routingPolicy, setRoutingPolicy] = useState( +    RoutingPolicy.Automatic +  ); +  const [temperature, setTemperature] = useState(0.7); +  const [maxTokens, setMaxTokens] = useState(10000); +  const [systemPrompt, setSystemPrompt] = useState(''); +  const [apiKeyConfigured, setApiKeyConfigured] = useState(false); + +  // API Configuration state +  const [apiKey, setApiKey] = useState(''); +  const [baseURL, setBaseURL] = useState(''); +  const [isBaseURLConfigured, setIsBaseURLConfigured] = useState(false); +  const [showApiConfigModal, setShowApiConfigModal] = useState(false); +  const [showPassword, setShowPassword] = useState(false); + +  // Storage state +  const [storageInfo, setStorageInfo] = +    useState(DEFAULT_STORAGE_INFO); +  const [_isRefreshing, setIsRefreshing] = useState(false); +  const [sdkVersion, setSdkVersion] = useState('0.1.0'); + +  // SDK State +  const [capabilities, setCapabilities] = useState([]); +  const [backendInfoData, setBackendInfoData] = useState< +    Record +  >({}); +  const [isSTTLoaded, setIsSTTLoaded] = useState(false); +  const [isTTSLoaded, setIsTTSLoaded] = useState(false); +  const [isTextLoaded, setIsTextLoaded] = useState(false); +  const [isVADLoaded, setIsVADLoaded] = useState(false); +  const [_memoryUsage, _setMemoryUsage] = useState(0); + +  // Model catalog state +  const [availableModels, setAvailableModels] = useState([]); +  const [downloadingModels, setDownloadingModels] = useState< +    Record +  >({}); +  const [downloadedModels, setDownloadedModels] = useState([]); + +  // Tool calling state +  const [toolCallingEnabled, setToolCallingEnabled] = useState(false); +  const [registeredTools, setRegisteredTools] = useState}>>([]); + +  // Capability names mapping +  const capabilityNames: Record = { +    0: 'STT (Speech-to-Text)', +    1: 'TTS (Text-to-Speech)', +    2: 'Text Generation', +    3: 'Embeddings', +    4: 'VAD (Voice Activity)', +    5: 'Diarization', +  }; + +  // Load data on mount +  useEffect(() => { +    loadData(); +    loadApiConfiguration(); +    loadGenerationSettings(); +    loadToolCallingSettings(); +  }, []); + +  /** +   * Load API configuration from AsyncStorage +   */ +  const loadApiConfiguration = async () => { +    try { +      const storedApiKey = await AsyncStorage.getItem(STORAGE_KEYS.API_KEY); +      const storedBaseURL = await AsyncStorage.getItem(STORAGE_KEYS.BASE_URL); + +      setApiKey(storedApiKey || ''); +      setBaseURL(storedBaseURL || ''); +      setApiKeyConfigured(!!storedApiKey && storedApiKey !== ''); +      setIsBaseURLConfigured(!!storedBaseURL && storedBaseURL !== ''); +    } catch (error) { +      console.error('[Settings] Failed to load API configuration:', error); +    } +  }; + +  /** +   * Load generation settings from AsyncStorage +   */ +  const loadGenerationSettings = async () => { +    try { +      const tempStr = await AsyncStorage.getItem(GENERATION_SETTINGS_KEYS.TEMPERATURE); +      const maxStr = await AsyncStorage.getItem(GENERATION_SETTINGS_KEYS.MAX_TOKENS); +      const sysStr = await AsyncStorage.getItem(GENERATION_SETTINGS_KEYS.SYSTEM_PROMPT); + +      const temperature = tempStr !== null ? parseFloat(tempStr) : 0.7; +      setTemperature(temperature); +      if (maxStr) setMaxTokens(parseInt(maxStr, 10)); +      if (sysStr) setSystemPrompt(sysStr); + +      console.log('[Settings] Loaded generation settings:', { +      temperature, +      maxTokens, +      systemPrompt: systemPrompt ? 'set' : 'empty', +    }); +    } catch (error) { +      console.error('[Settings] Failed to load generation settings:', error); +    } +  }; + +  /** +   * Save generation settings to AsyncStorage +   */ +  const saveGenerationSettings = async () => { +    try { +      await AsyncStorage.setItem(GENERATION_SETTINGS_KEYS.TEMPERATURE, temperature.toString()); +      await AsyncStorage.setItem(GENERATION_SETTINGS_KEYS.MAX_TOKENS, maxTokens.toString()); +      await AsyncStorage.setItem(GENERATION_SETTINGS_KEYS.SYSTEM_PROMPT, systemPrompt); + +      console.log('[Settings] Saved generation settings:', { +        temperature, +        maxTokens, +        systemPrompt: systemPrompt ? `set(${systemPrompt.length} chars)` : 'empty', +      }); + +      Alert.alert('Saved', 'Generation settings have been saved successfully.'); +    } catch (error) { +      console.error('[Settings] Failed to save generation settings:', error); +      Alert.alert('Error', `Failed to save settings: ${error}`); +    } +  }; + +  /** +   * Load tool calling settings from AsyncStorage +   */ +  const loadToolCallingSettings = async () => { +    try { +      const enabled = await AsyncStorage.getItem(STORAGE_KEYS.TOOL_CALLING_ENABLED); +      setToolCallingEnabled(enabled === 'true'); +      refreshRegisteredTools(); +    } catch (error) { +      console.error('[Settings] Failed to load tool calling settings:', error); +    } +  }; + +  /** +   * Refresh the list of registered tools from SDK +   */ +  const refreshRegisteredTools = () => { +    const tools = RunAnywhere.getRegisteredTools(); +    setRegisteredTools(tools.map(t => ({ +      name: t.name, +      description: t.description, +      parameters: t.parameters || [], +    }))); +  }; + +  /** +   * Toggle tool calling enabled state +   */ +  const handleToggleToolCalling = async (enabled: boolean) => { +    setToolCallingEnabled(enabled); +    try { +      await AsyncStorage.setItem(STORAGE_KEYS.TOOL_CALLING_ENABLED, enabled ? 'true' : 'false'); +    } catch (error) { +      console.error('[Settings] Failed to save tool calling setting:', error); +    } +  }; + +  /** +   * Register demo tools (weather, time, calculator) +   */ +  const registerDemoTools = () => { +    // Clear existing tools +    RunAnywhere.clearTools(); + +    // Weather tool - Real API (wttr.in - no key needed) +    RunAnywhere.registerTool( +      { +        name: 'get_weather', +        description: 'Gets the current weather for a city or location', +        parameters: [ +          { +            name: 'location', +            type: 'string', +            description: 'City name or location (e.g., "Tokyo", "New York", "London")', +            required: true, +          }, +        ], +      }, +      async (args: Record) => { +        const location = (args.location as string) || 'San Francisco'; +        try { +          const response = await fetch( +            `https://wttr.in/${encodeURIComponent(location)}?format=j1` +          ); +          const data = await response.json(); +          const current = data.current_condition?.[0]; +          return { +            location, +            temperature_c: current?.temp_C || 'N/A', +            temperature_f: current?.temp_F || 'N/A', +            condition: current?.weatherDesc?.[0]?.value || 'Unknown', +            humidity: current?.humidity || 'N/A', +            wind_kph: current?.windspeedKmph || 'N/A', +          }; +        } catch (error) { +          return { error: `Failed to get weather: ${error}` }; +        } +      } +    ); + +    // Time tool - Real system time +    RunAnywhere.registerTool( +      { +        name: 'get_current_time', +        description: 'Gets the current date, time, and timezone information', +        parameters: [], +      }, +      async () => { +        const now = new Date(); +        return { +          datetime: now.toLocaleString(), +          time: now.toLocaleTimeString(), +          timestamp: now.toISOString(), +          timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, +        }; +      } +    ); + +    // Calculator tool - Math evaluation +    RunAnywhere.registerTool( +      { +        name: 'calculate', +        description: 'Performs math calculations. Supports +, -, *, /, and parentheses', +        parameters: [ +          { +            name: 'expression', +            type: 'string', +            description: 'Math expression (e.g., "2 + 2 * 3", "(10 + 5) / 3")', +            required: true, +          }, +        ], +      }, +      async (args: Record) => { +        const expression = (args.expression as string) || '0'; +        try { +          // Safe math evaluation using recursive descent parser +          const result = safeEvaluateExpression(expression); +          return { +            expression: expression, +            result: result, +          }; +        } catch (error) { +          return { error: `Failed to calculate: ${error}` }; +        } +      } +    ); + +    refreshRegisteredTools(); +    Alert.alert('Demo Tools Added', '3 demo tools have been registered: get_weather, get_current_time, calculate'); +  }; + +  /** +   * Clear all registered tools +   */ +  const clearAllTools = () => { +    Alert.alert( +      'Clear All Tools', +      'Are you sure you want to remove all registered tools?', +      [ +        { text: 'Cancel', style: 'cancel' }, +        { +          text: 'Clear', +          style: 'destructive', +          onPress: () => { +            RunAnywhere.clearTools(); +            refreshRegisteredTools(); +          }, +        }, +      ] +    ); +  }; + +  /** +   * Normalize base URL by adding https:// if no scheme is present +   */ +  const normalizeBaseURL = (url: string): string => { +    const trimmed = url.trim(); +    if (!trimmed) return trimmed; +    if (trimmed.startsWith('http://') || trimmed.startsWith('https://')) { +      return trimmed; +    } +    return `https://${trimmed}`; +  }; + +  /** +   * Save API configuration to AsyncStorage +   */ +  const saveApiConfiguration = async () => { +    try { +      const normalizedURL = normalizeBaseURL(baseURL); +      await AsyncStorage.setItem(STORAGE_KEYS.API_KEY, apiKey); +      await AsyncStorage.setItem(STORAGE_KEYS.BASE_URL, normalizedURL); + +      setBaseURL(normalizedURL); +      setApiKeyConfigured(!!apiKey); +      setIsBaseURLConfigured(!!normalizedURL); +      setShowApiConfigModal(false); + +      Alert.alert( +        'Restart Required', +        'API configuration has been updated. Please restart the app for changes to take effect.', +        [{ text: 'OK' }] +      ); +    } catch (error) { +      Alert.alert('Error', `Failed to save API configuration: ${error}`); +    } +  }; + +  /** +   * Clear API configuration from AsyncStorage +   */ +  const clearApiConfiguration = async () => { +    try { +      await AsyncStorage.multiRemove([ +        STORAGE_KEYS.API_KEY, +        STORAGE_KEYS.BASE_URL, +        STORAGE_KEYS.DEVICE_REGISTERED, +      ]); + +      setApiKey(''); +      setBaseURL(''); +      setApiKeyConfigured(false); +      setIsBaseURLConfigured(false); + +      Alert.alert( +        'Restart Required', +        'API configuration has been cleared. Please restart the app for changes to take effect.', +        [{ text: 'OK' }] +      ); +    } catch (error) { +      Alert.alert('Error', `Failed to clear API configuration: ${error}`); +    } +  }; + +  const loadData = async () => { +    setIsRefreshing(true); +    try { +      // Get SDK version +      const version = await RunAnywhere.getVersion(); +      setSdkVersion(version); + +      // Check if SDK is initialized first +      const isInit = await RunAnywhere.isInitialized(); +      console.log('[Settings] SDK isInitialized:', isInit); + +      // Get backend info for storage data +      const backendInfo = await RunAnywhere.getBackendInfo(); +      console.log('[Settings] Backend info:', backendInfo); + +      // Override name with actual init status +      const updatedBackendInfo = { +        ...backendInfo, +        name: isInit ? 'RunAnywhere Core' : 'Not initialized', +        version: version, +        initialized: isInit, +      }; +      setBackendInfoData(updatedBackendInfo); + +      // Get capabilities (returns string[], not number[]) +      const caps = await RunAnywhere.getCapabilities(); +      console.warn('[Settings] Capabilities:', caps); +      // Convert string capabilities to numbers for display mapping +      const capNumbers = caps.map((cap, index) => index); +      setCapabilities(capNumbers); + +      // Check loaded models +      const sttLoaded = await RunAnywhere.isSTTModelLoaded(); +      const ttsLoaded = await RunAnywhere.isTTSModelLoaded(); +      const textLoaded = await RunAnywhere.isModelLoaded(); +      const vadLoaded = await RunAnywhere.isVADModelLoaded(); + +      setIsSTTLoaded(sttLoaded); +      setIsTTSLoaded(ttsLoaded); +      setIsTextLoaded(textLoaded); +      setIsVADLoaded(vadLoaded); + +      console.warn( +        '[Settings] Models loaded - STT:', +        sttLoaded, +        'TTS:', +        ttsLoaded, +        'Text:', +        textLoaded, +        'VAD:', +        vadLoaded +      ); + +      // Get available models from catalog +      try { +        const available = await RunAnywhere.getAvailableModels(); +        console.warn('[Settings] Available models:', available); +        setAvailableModels(available); +      } catch (err) { +        console.warn('[Settings] Failed to get available models:', err); +      } + +      // Get downloaded models +      try { +        const downloaded = await RunAnywhere.getDownloadedModels(); +        console.warn('[Settings] Downloaded models:', downloaded); +        setDownloadedModels(downloaded); +      } catch (err) { +        console.warn('[Settings] Failed to get downloaded models:', err); +      } + +      // Get storage info using new SDK API +      try { +        const storage = await RunAnywhere.getStorageInfo(); +        console.warn('[Settings] Storage info:', storage); +        setStorageInfo({ +          totalStorage: storage.deviceStorage.totalSpace, +          appStorage: storage.appStorage.totalSize, +          modelsStorage: storage.modelStorage.totalSize, +          cacheSize: storage.cacheSize, +          freeSpace: storage.deviceStorage.freeSpace, +        }); +      } catch (err) { +        console.warn('[Settings] Failed to get storage info:', err); +      } +    } catch (error) { +      console.error('Failed to load data:', error); +    } finally { +      setIsRefreshing(false); +    } +  }; + +  /** +   * Handle routing policy change +   */ +  const handleRoutingPolicyChange = useCallback(() => { +    const policies = Object.values(RoutingPolicy); +    Alert.alert( +      'Routing Policy', +      'Choose how requests are routed', +      policies.map((policy) => ({ +        text: RoutingPolicyDisplayNames[policy], +        onPress: () => { +          setRoutingPolicy(policy); +        }, +      })) +    ); +  }, []); + +  /** +   * Handle API key configuration - open modal +   */ +  const handleConfigureApiKey = useCallback(() => { +    setShowApiConfigModal(true); +  }, []); + +  /** +   * Cancel API configuration modal +   */ +  const handleCancelApiConfig = useCallback(() => { +    loadApiConfiguration(); // Reset to stored values +    setShowApiConfigModal(false); +  }, []); + + + +  /** +   * Handle clear cache +   */ +  const handleClearCache = useCallback(() => { +    Alert.alert( +      'Clear Cache', +      'This will clear temporary files. Models will not be deleted.', +      [ +        { text: 'Cancel', style: 'cancel' }, +        { +          text: 'Clear', +          style: 'destructive', +          onPress: async () => { +            try { +              // Clear SDK cache using new Storage API +              await RunAnywhere.clearCache(); +              await RunAnywhere.cleanTempFiles(); +              Alert.alert('Success', 'Cache cleared successfully'); +              loadData(); +            } catch (err) { +              console.error('[Settings] Failed to clear cache:', err); +              Alert.alert('Error', `Failed to clear cache: ${err}`); +            } +          }, +        }, +      ] +    ); +  }, []); + +  /** +   * Handle model download +   */ +  const handleDownloadModel = useCallback( +    async (model: ModelInfo) => { +      if (downloadingModels[model.id] !== undefined) { +        // Already downloading, cancel it +        try { +          await RunAnywhere.cancelDownload(model.id); +          setDownloadingModels((prev) => { +            const updated = { ...prev }; +            delete updated[model.id]; +            return updated; +          }); +        } catch (err) { +          console.error('Failed to cancel download:', err); +        } +        return; +      } + +      // Start download with progress tracking +      setDownloadingModels((prev) => ({ ...prev, [model.id]: 0 })); + +      try { +        await RunAnywhere.downloadModel(model.id, (progress) => { +          console.warn( +            `[Settings] Download progress for ${model.id}: ${(progress.progress * 100).toFixed(1)}%` +          ); +          setDownloadingModels((prev) => ({ +            ...prev, +            [model.id]: progress.progress, +          })); +        }); + +        // Download complete +        setDownloadingModels((prev) => { +          const updated = { ...prev }; +          delete updated[model.id]; +          return updated; +        }); + +        Alert.alert('Success', `${model.name} downloaded successfully!`); +        loadData(); // Refresh to show downloaded model +      } catch (err) { +        setDownloadingModels((prev) => { +          const updated = { ...prev }; +          delete updated[model.id]; +          return updated; +        }); +        Alert.alert( +          'Download Failed', +          `Failed to download ${model.name}: ${err}` +        ); +      } +    }, +    [downloadingModels] +  ); + +  /** +   * Handle delete downloaded model +   */ +  const handleDeleteDownloadedModel = useCallback(async (model: ModelInfo) => { +    const downloadedModel = downloadedModels.find((m) => m.id === model.id); +    // Prefer downloaded model's size (actual disk usage) over catalog downloadSize (expected size) +    // TODO: Replace with actual disk size once SDK exposes it (e.g., sizeOnDisk or actualSize) +    const freedSize = +      downloadedModel?.downloadSize ?? // Use downloaded model's size when available +      model.downloadSize ?? +      0; + +    Alert.alert( +      'Delete Model', +      `Are you sure you want to delete ${model.name}? This will free up ${formatBytes(freedSize)}.`, +      [ +        { text: 'Cancel', style: 'cancel' }, +        { +          text: 'Delete', +          style: 'destructive', +          onPress: async () => { +            try { +              await RunAnywhere.deleteModel(model.id); +              Alert.alert('Deleted', `${model.name} has been deleted.`); +              loadData(); // Refresh list +            } catch (err) { +              Alert.alert('Error', `Failed to delete: ${err}`); +            } +          }, +        }, +      ] +    ); +  }, [downloadedModels]); + +  /** +   * Handle clear all data +   */ +  const handleClearAllData = useCallback(() => { +    Alert.alert( +      'Clear All Data', +      'This will delete all models and reset the app. This action cannot be undone.', +      [ +        { text: 'Cancel', style: 'cancel' }, +        { +          text: 'Clear All', +          style: 'destructive', +          onPress: async () => { +            try { +              // Unload all models +              await RunAnywhere.unloadModel(); +              await RunAnywhere.unloadSTTModel(); +              await RunAnywhere.unloadTTSModel(); +              // Destroy SDK +              await RunAnywhere.destroy(); +              Alert.alert('Success', 'All data cleared'); +            } catch (error) { +              Alert.alert('Error', `Failed to clear data: ${error}`); +            } +            loadData(); +          }, +        }, +      ] +    ); +  }, []); + +  /** +   * Render section header +   */ +  const renderSectionHeader = (title: string) => ( +    {title} +  ); + +  /** +   * Render setting row +   */ +  const renderSettingRow = ( +    icon: string, +    title: string, +    value: string, +    onPress?: () => void, +    showChevron: boolean = true +  ) => ( +    +      +        +        {title} +      +      +        {value} +        {showChevron && onPress && ( +          +        )} +      +    +  ); + +  /** +   * Render slider setting +   */ +  const renderSliderSetting = ( +    title: string, +    value: number, +    onChange: (value: number) => void, +    min: number, +    max: number, +    step: number, +    formatValue: (v: number) => string +  ) => ( +    +      +        {title} +        {formatValue(value)} +      +      +        onChange(Math.max(min, value - step))} +        > +          +        +        +          +        +        onChange(Math.min(max, value + step))} +        > +          +        +      +    +  ); + +  /** +   * Render storage bar +   * Matches iOS: shows app storage usage relative to device free space +   */ +  const renderStorageBar = () => { +    // Show app storage as portion of (app storage + free space) +    const totalAvailable = storageInfo.appStorage + storageInfo.freeSpace; +    const usedPercent = totalAvailable > 0 +      ? (storageInfo.appStorage / totalAvailable) * 100 +      : 0; +    return ( +      +        +          +        +        +          {formatBytes(storageInfo.appStorage)} of{' '} +          {formatBytes(storageInfo.freeSpace)} available +        +      +    ); +  }; + + + +  /** +   * Render catalog model row +   */ +  const renderCatalogModelRow = (model: ModelInfo) => { +    const isDownloading = downloadingModels[model.id] !== undefined; +    const downloadProgress = downloadingModels[model.id] || 0; +    const isDownloaded = downloadedModels.some((m) => m.id === model.id); + +    // Determine framework based on format +    const framework = model.format === 'onnx' ? LLMFramework.ONNX : LLMFramework.LlamaCpp; +    const frameworkName = FrameworkDisplayNames[framework] || framework; + +    // Get model size estimate based on download size (may differ from actual on-disk size) +    const downloadedModel = downloadedModels.find((m) => m.id === model.id); +    const modelSize = +      downloadedModel?.downloadSize ?? // Prefer size from downloaded model when available +      model.downloadSize ?? // Fall back to catalog's expected download size +      0; +    return ( +      +        +          +            {model.name} +            +              {model.category} +            +          +          {model.metadata?.description && ( +            +              {model.metadata.description} +            +          )} +          +            +              {formatBytes(modelSize)} +            +            {frameworkName} +          +          {isDownloading && ( +            +              +                +              +              +                {(downloadProgress * 100).toFixed(0)}% +              +            +          )} +        +        +            isDownloaded +              ? handleDeleteDownloadedModel(model) +              : handleDownloadModel(model) +          } +        > +          +        +      +    ); +  }; + +  return ( +    +      {/* Header */} +      +        Settings +        +          +        +      + +      +        {/* Generation Settings - Matches iOS CombinedSettingsView order */} +        {renderSectionHeader('Generation Settings')} +        +          {renderSliderSetting( +            'Temperature', +            temperature, +            setTemperature, +            SETTINGS_CONSTRAINTS.temperature.min, +            SETTINGS_CONSTRAINTS.temperature.max, +            SETTINGS_CONSTRAINTS.temperature.step, +            (v) => v.toFixed(1) +          )} +          {renderSliderSetting( +            'Max Tokens', +            maxTokens, +            setMaxTokens, +            SETTINGS_CONSTRAINTS.maxTokens.min, +            SETTINGS_CONSTRAINTS.maxTokens.max, +            SETTINGS_CONSTRAINTS.maxTokens.step, +            (v) => v.toLocaleString() +          )} + +          {/* System Prompt Input */} +          +            System Prompt +            +          + +          {/* Save Settings Button */} +          +            +            Save Settings +          +        + +        {/* API Configuration (Testing) */} +        {renderSectionHeader('API Configuration (Testing)')} +        +          +            API Key +            +              {apiKeyConfigured ? 'Configured' : 'Not Set'} +            +          +          +          +            Base URL +            +              {isBaseURLConfigured ? 'Configured' : 'Not Set'} +            +          +          +          +            +              Configure +            +            {apiKeyConfigured && isBaseURLConfigured && ( +              +                Clear +              +            )} +          +          +            Configure custom API key and base URL for testing. Requires app restart. +          +        + +        {/* Tool Settings - Matches iOS ToolSettingsView */} +        {renderSectionHeader('Tool Settings')} +        +          {/* Enable Tool Calling Toggle */} +          +            +              Enable Tool Calling +              +                Allow LLMs to call tools (APIs, functions) +              +            +            handleToggleToolCalling(!toolCallingEnabled)} +            > +              +            +          + +          {toolCallingEnabled && ( +            <> +              +               +              {/* Registered Tools Count */} +              +                Registered Tools +                0 ? Colors.primaryGreen : Colors.textSecondary }]}> +                  {registeredTools.length} {registeredTools.length === 1 ? 'tool' : 'tools'} +                +              + +              {/* Demo Tools Button */} +              {registeredTools.length === 0 && ( +                <> +                  +                  +                    +                    Add Demo Tools +                  +                +              )} + +              {/* Registered Tools List */} +              {registeredTools.length > 0 && ( +                <> +                  +                  {registeredTools.map((tool, index) => ( +                    +                      +                      +                        {tool.name} +                        {tool.description} +                        {tool.parameters.length > 0 && ( +                          +                            {tool.parameters.map((p) => ( +                              +                                {p.name} +                              +                            ))} +                          +                        )} +                      +                      {index < registeredTools.length - 1 && } +                    +                  ))} + +                  {/* Clear All Tools Button */} +                  +                  +                    +                    Clear All Tools +                  +                +              )} +            +          )} + +          +            Tools allow the LLM to call external APIs and functions to get real-time data. +          +        + +        {/* Storage Overview - Matches iOS CombinedSettingsView */} +        {renderSectionHeader('Storage Overview')} +        +          {renderStorageBar()} +          +            {/* Total Storage - App's total storage usage */} +            +              Total Storage +              +                {formatBytes(storageInfo.appStorage)} +              +            +            {/* Models Storage - Downloaded models size */} +            +              Models +              +                {formatBytes(storageInfo.modelsStorage)} +              +            +            {/* Cache Size */} +            +              Cache +              +                {formatBytes(storageInfo.cacheSize)} +              +            +            {/* Available - Device free space */} +            +              Available +              +                {formatBytes(storageInfo.freeSpace)} +              +            +          +        + +        {/* Model Catalog */} +        {renderSectionHeader('Model Catalog')} +        +          {availableModels.length === 0 ? ( +            Loading models... +          ) : ( +            availableModels.map(renderCatalogModelRow) +          )} +        + +        {/* Storage Management */} +        {renderSectionHeader('Storage Management')} +        +          +            +            Clear Cache +          +          +            +            +              Clear All Data +            +          +        + +        {/* Version Info */} +        +          RunAnywhere AI +          SDK v{sdkVersion} +        +      + +      {/* API Configuration Modal */} +      +        +          +            API Configuration + +            {/* API Key Input */} +            +              API Key +              +                +                setShowPassword(!showPassword)} +                > +                  +                +              +              +                Your API key for authenticating with the backend +              +            + +            {/* Base URL Input */} +            +              Base URL +              +              +                The backend API URL (https:// added automatically if missing) +              +            + +            {/* Warning */} +            +              +              +                After saving, you must restart the app for changes to take effect. The SDK will reinitialize with your custom configuration. +              +            + +            {/* Buttons */} +            +              +                Cancel +              +              +                Save +              +            +          +        +      +    +  ); }; const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: Colors.backgroundGrouped, - }, - header: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - paddingHorizontal: Padding.padding16, - paddingVertical: Padding.padding12, - backgroundColor: Colors.backgroundPrimary, - borderBottomWidth: 1, - borderBottomColor: Colors.borderLight, - }, - title: { - ...Typography.title2, - color: Colors.textPrimary, - }, - refreshButton: { - padding: Spacing.small, - }, - content: { - flex: 1, - }, - sectionHeader: { - ...Typography.footnote, - color: Colors.textSecondary, - textTransform: 'uppercase', - marginTop: Spacing.xLarge, - marginBottom: Spacing.small, - marginHorizontal: Padding.padding16, - }, - section: { - backgroundColor: Colors.backgroundPrimary, - borderRadius: BorderRadius.medium, - marginHorizontal: Padding.padding16, - overflow: 'hidden', - }, - settingRow: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - padding: Padding.padding16, - borderBottomWidth: 1, - borderBottomColor: Colors.borderLight, - }, - settingRowLeft: { - flexDirection: 'row', - alignItems: 'center', - gap: Spacing.medium, - }, - settingRowRight: { - flexDirection: 'row', - alignItems: 'center', - gap: Spacing.small, - }, - settingLabel: { - ...Typography.body, - color: Colors.textPrimary, - }, - settingValue: { - ...Typography.body, - color: Colors.textSecondary, - }, - sliderSetting: { - padding: Padding.padding16, - borderBottomWidth: 1, - borderBottomColor: Colors.borderLight, - }, - sliderHeader: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - marginBottom: Spacing.medium, - }, - sliderValue: { - ...Typography.body, - color: Colors.primaryBlue, - fontWeight: '600', - }, - sliderControls: { - flexDirection: 'row', - alignItems: 'center', - gap: Spacing.medium, - }, - sliderButton: { - width: 36, - height: 36, - borderRadius: 18, - backgroundColor: Colors.backgroundSecondary, - justifyContent: 'center', - alignItems: 'center', - }, - sliderTrack: { - flex: 1, - height: 6, - backgroundColor: Colors.backgroundGray5, - borderRadius: 3, - }, - sliderFill: { - height: '100%', - backgroundColor: Colors.primaryBlue, - borderRadius: 3, - }, - storageBar: { - padding: Padding.padding16, - }, - storageBarTrack: { - height: 8, - backgroundColor: Colors.backgroundGray5, - borderRadius: 4, - overflow: 'hidden', - }, - storageBarFill: { - height: '100%', - backgroundColor: Colors.primaryBlue, - borderRadius: 4, - }, - storageText: { - ...Typography.footnote, - color: Colors.textSecondary, - marginTop: Spacing.small, - textAlign: 'center', - }, - storageDetails: { - borderTopWidth: 1, - borderTopColor: Colors.borderLight, - padding: Padding.padding16, - gap: Spacing.small, - }, - storageDetailRow: { - flexDirection: 'row', - justifyContent: 'space-between', - }, - storageDetailLabel: { - ...Typography.subheadline, - color: Colors.textSecondary, - }, - storageDetailValue: { - ...Typography.subheadline, - color: Colors.textPrimary, - }, - modelRow: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - padding: Padding.padding16, - borderBottomWidth: 1, - borderBottomColor: Colors.borderLight, - }, - modelInfo: { - flex: 1, - }, - modelName: { - ...Typography.body, - color: Colors.textPrimary, - }, - modelMeta: { - flexDirection: 'row', - alignItems: 'center', - gap: Spacing.small, - marginTop: Spacing.xSmall, - }, - frameworkBadge: { - backgroundColor: Colors.badgeBlue, - paddingHorizontal: Spacing.small, - paddingVertical: Spacing.xxSmall, - borderRadius: BorderRadius.small, - }, - frameworkBadgeText: { - ...Typography.caption2, - color: Colors.primaryBlue, - fontWeight: '600', - }, - modelSize: { - ...Typography.caption, - color: Colors.textSecondary, - }, - deleteButton: { - padding: Spacing.small, - }, - emptyText: { - ...Typography.body, - color: Colors.textSecondary, - textAlign: 'center', - padding: Padding.padding24, - }, - dangerButton: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - gap: Spacing.smallMedium, - padding: Padding.padding16, - borderBottomWidth: 1, - borderBottomColor: Colors.borderLight, - }, - dangerButtonRed: { - borderBottomWidth: 0, - }, - dangerButtonText: { - ...Typography.body, - color: Colors.primaryOrange, - }, - dangerButtonTextRed: { - color: Colors.primaryRed, - }, - versionContainer: { - alignItems: 'center', - padding: Padding.padding24, - marginTop: Spacing.large, - marginBottom: Spacing.xxxLarge, - }, - versionText: { - ...Typography.footnote, - color: Colors.textTertiary, - }, - versionSubtext: { - ...Typography.caption, - color: Colors.textTertiary, - marginTop: Spacing.xSmall, - }, - // SDK Status styles - statusRow: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - padding: Padding.padding16, - borderBottomWidth: 1, - borderBottomColor: Colors.borderLight, - }, - statusLabel: { - ...Typography.body, - color: Colors.textPrimary, - }, - statusValue: { - ...Typography.body, - color: Colors.primaryBlue, - fontWeight: '600', - }, - capabilitiesContainer: { - padding: Padding.padding16, - borderBottomWidth: 1, - borderBottomColor: Colors.borderLight, - }, - capabilitiesLabel: { - ...Typography.subheadline, - color: Colors.textSecondary, - marginBottom: Spacing.small, - }, - capabilitiesList: { - flexDirection: 'row', - flexWrap: 'wrap', - gap: Spacing.small, - }, - capabilityBadge: { - flexDirection: 'row', - alignItems: 'center', - gap: Spacing.xSmall, - backgroundColor: Colors.badgeGreen, - paddingHorizontal: Spacing.smallMedium, - paddingVertical: Spacing.xSmall, - borderRadius: BorderRadius.small, - }, - capabilityText: { - ...Typography.caption, - color: Colors.primaryGreen, - fontWeight: '600', - }, - noCapabilities: { - ...Typography.body, - color: Colors.textTertiary, - fontStyle: 'italic', - }, - modelStatusContainer: { - padding: Padding.padding16, - }, - modelStatusGrid: { - flexDirection: 'row', - flexWrap: 'wrap', - gap: Spacing.small, - marginTop: Spacing.small, - }, - modelStatusItem: { - flexDirection: 'row', - alignItems: 'center', - gap: Spacing.xSmall, - backgroundColor: Colors.backgroundSecondary, - paddingHorizontal: Spacing.medium, - paddingVertical: Spacing.small, - borderRadius: BorderRadius.small, - }, - modelStatusItemLoaded: { - backgroundColor: Colors.badgeGreen, - }, - modelStatusText: { - ...Typography.footnote, - color: Colors.textSecondary, - fontWeight: '600', - }, - // Model catalog styles - catalogModelRow: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - padding: Padding.padding16, - borderBottomWidth: 1, - borderBottomColor: Colors.borderLight, - }, - catalogModelInfo: { - flex: 1, - marginRight: Spacing.medium, - }, - catalogModelHeader: { - flexDirection: 'row', - alignItems: 'center', - gap: Spacing.small, - marginBottom: Spacing.xSmall, - }, - catalogModelName: { - ...Typography.body, - color: Colors.textPrimary, - fontWeight: '600', - }, - catalogModelBadge: { - backgroundColor: Colors.badgeBlue, - paddingHorizontal: Spacing.small, - paddingVertical: Spacing.xxSmall, - borderRadius: BorderRadius.small, - }, - catalogModelBadgeText: { - ...Typography.caption2, - color: Colors.primaryBlue, - fontWeight: '600', - textTransform: 'uppercase', - }, - catalogModelDescription: { - ...Typography.footnote, - color: Colors.textSecondary, - marginBottom: Spacing.xSmall, - }, - catalogModelMeta: { - flexDirection: 'row', - alignItems: 'center', - gap: Spacing.small, - }, - catalogModelSize: { - ...Typography.caption, - color: Colors.textTertiary, - }, - catalogModelFormat: { - ...Typography.caption, - color: Colors.textTertiary, - }, - catalogModelButton: { - width: 44, - height: 44, - borderRadius: 22, - backgroundColor: Colors.backgroundSecondary, - justifyContent: 'center', - alignItems: 'center', - }, - catalogModelButtonDownloaded: { - backgroundColor: Colors.badgeGreen, - }, - catalogModelButtonDownloading: { - backgroundColor: Colors.badgeOrange, - }, - downloadProgressContainer: { - flexDirection: 'row', - alignItems: 'center', - gap: Spacing.small, - marginTop: Spacing.small, - }, - downloadProgressTrack: { - flex: 1, - height: 4, - backgroundColor: Colors.backgroundGray5, - borderRadius: 2, - overflow: 'hidden', - }, - downloadProgressFill: { - height: '100%', - backgroundColor: Colors.primaryBlue, - borderRadius: 2, - }, - downloadProgressText: { - ...Typography.caption, - color: Colors.primaryBlue, - fontWeight: '600', - minWidth: 40, - textAlign: 'right', - }, - // API Configuration styles - apiConfigRow: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - padding: Padding.padding16, - }, - apiConfigLabel: { - ...Typography.body, - color: Colors.textPrimary, - }, - apiConfigValue: { - ...Typography.body, - fontWeight: '500', - }, - apiConfigDivider: { - height: 1, - backgroundColor: Colors.borderLight, - marginHorizontal: Padding.padding16, - }, - apiConfigButtons: { - flexDirection: 'row', - padding: Padding.padding16, - gap: Spacing.small, - }, - apiConfigButton: { - paddingHorizontal: Padding.padding16, - paddingVertical: Spacing.small, - borderRadius: BorderRadius.small, - borderWidth: 1, - borderColor: Colors.primaryBlue, - }, - apiConfigButtonClear: { - borderColor: Colors.primaryRed, - }, - apiConfigButtonText: { - ...Typography.subheadline, - color: Colors.primaryBlue, - fontWeight: '600', - }, - apiConfigButtonTextClear: { - color: Colors.primaryRed, - }, - apiConfigHint: { - ...Typography.footnote, - color: Colors.textSecondary, - paddingHorizontal: Padding.padding16, - paddingBottom: Padding.padding16, - }, - // Modal styles - modalOverlay: { - flex: 1, - backgroundColor: 'rgba(0, 0, 0, 0.5)', - justifyContent: 'center', - alignItems: 'center', - padding: Padding.padding24, - }, - modalContent: { - backgroundColor: Colors.backgroundPrimary, - borderRadius: BorderRadius.large, - padding: Padding.padding24, - width: '100%', - maxWidth: 400, - }, - modalTitle: { - ...Typography.title2, - color: Colors.textPrimary, - marginBottom: Spacing.large, - textAlign: 'center', - }, - inputGroup: { - marginBottom: Spacing.large, - }, - inputLabel: { - ...Typography.subheadline, - color: Colors.textSecondary, - marginBottom: Spacing.small, - }, - input: { - backgroundColor: Colors.backgroundSecondary, - borderRadius: BorderRadius.small, - padding: Padding.padding12, - ...Typography.body, - color: Colors.textPrimary, - borderWidth: 1, - borderColor: Colors.borderLight, - }, - passwordInputContainer: { - flexDirection: 'row', - alignItems: 'center', - backgroundColor: Colors.backgroundSecondary, - borderRadius: BorderRadius.small, - borderWidth: 1, - borderColor: Colors.borderLight, - }, - passwordInput: { - flex: 1, - padding: Padding.padding12, - ...Typography.body, - color: Colors.textPrimary, - }, - passwordToggle: { - padding: Padding.padding12, - }, - inputHint: { - ...Typography.caption, - color: Colors.textTertiary, - marginTop: Spacing.xSmall, - }, - warningBox: { - flexDirection: 'row', - backgroundColor: Colors.badgeOrange, - borderRadius: BorderRadius.small, - padding: Padding.padding12, - gap: Spacing.small, - marginBottom: Spacing.large, - }, - warningText: { - ...Typography.footnote, - color: Colors.textSecondary, - flex: 1, - }, - modalButtons: { - flexDirection: 'row', - justifyContent: 'flex-end', - gap: Spacing.medium, - }, - modalButton: { - paddingHorizontal: Padding.padding16, - paddingVertical: Spacing.smallMedium, - borderRadius: BorderRadius.small, - minWidth: 80, - alignItems: 'center', - }, - modalButtonCancel: { - backgroundColor: 'transparent', - }, - modalButtonSave: { - backgroundColor: Colors.primaryBlue, - }, - modalButtonDisabled: { - backgroundColor: Colors.backgroundGray5, - }, - modalButtonTextCancel: { - ...Typography.body, - color: Colors.textSecondary, - }, - modalButtonTextSave: { - ...Typography.body, - color: Colors.textWhite, - fontWeight: '600', - }, - modalButtonTextDisabled: { - color: Colors.textTertiary, - }, - // Tool Settings styles - toolSettingRow: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - padding: Padding.padding16, - }, - toolSettingInfo: { - flex: 1, - marginRight: Spacing.medium, - }, - toolSettingLabel: { - ...Typography.body, - color: Colors.textPrimary, - fontWeight: '500', - }, - toolSettingDescription: { - ...Typography.footnote, - color: Colors.textSecondary, - marginTop: Spacing.xxSmall, - }, - toolSettingValue: { - ...Typography.body, - fontWeight: '600', - }, - toggleButton: { - width: 51, - height: 31, - borderRadius: 15.5, - backgroundColor: Colors.backgroundGray5, - padding: 2, - justifyContent: 'center', - }, - toggleButtonActive: { - backgroundColor: Colors.primaryBlue, - }, - toggleKnob: { - width: 27, - height: 27, - borderRadius: 13.5, - backgroundColor: Colors.textWhite, - shadowColor: '#000', - shadowOffset: { width: 0, height: 2 }, - shadowOpacity: 0.2, - shadowRadius: 2, - elevation: 2, - }, - toggleKnobActive: { - alignSelf: 'flex-end', - }, - demoToolsButton: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - gap: Spacing.small, - padding: Padding.padding16, - }, - demoToolsButtonText: { - ...Typography.body, - color: Colors.primaryBlue, - fontWeight: '600', - }, - toolRow: { - flexDirection: 'row', - alignItems: 'flex-start', - padding: Padding.padding16, - gap: Spacing.medium, - }, - toolInfo: { - flex: 1, - }, - toolName: { - ...Typography.subheadline, - color: Colors.textPrimary, - fontWeight: '600', - }, - toolDescription: { - ...Typography.footnote, - color: Colors.textSecondary, - marginTop: Spacing.xxSmall, - }, - toolParams: { - flexDirection: 'row', - flexWrap: 'wrap', - gap: Spacing.xSmall, - marginTop: Spacing.small, - }, - toolParamChip: { - backgroundColor: Colors.badgeBlue, - paddingHorizontal: Spacing.small, - paddingVertical: Spacing.xxSmall, - borderRadius: BorderRadius.small, - }, - toolParamText: { - ...Typography.caption2, - color: Colors.primaryBlue, - fontWeight: '500', - }, - clearToolsButton: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - gap: Spacing.small, - padding: Padding.padding16, - }, - clearToolsButtonText: { - ...Typography.body, - color: Colors.statusRed, - fontWeight: '600', - }, +  container: { +    flex: 1, +    backgroundColor: Colors.backgroundGrouped, +  }, +  header: { +    flexDirection: 'row', +    alignItems: 'center', +    justifyContent: 'space-between', +    paddingHorizontal: Padding.padding16, +    paddingVertical: Padding.padding12, +    backgroundColor: Colors.backgroundPrimary, +    borderBottomWidth: 1, +    borderBottomColor: Colors.borderLight, +  }, +  title: { +    ...Typography.title2, +    color: Colors.textPrimary, +  }, +  refreshButton: { +    padding: Spacing.small, +  }, +  content: { +    flex: 1, +  }, +  sectionHeader: { +    ...Typography.footnote, +    color: Colors.textSecondary, +    textTransform: 'uppercase', +    marginTop: Spacing.xLarge, +    marginBottom: Spacing.small, +    marginHorizontal: Padding.padding16, +  }, +  section: { +    backgroundColor: Colors.backgroundPrimary, +    borderRadius: BorderRadius.medium, +    marginHorizontal: Padding.padding16, +    overflow: 'hidden', +  }, +  settingRow: { +    flexDirection: 'row', +    alignItems: 'center', +    justifyContent: 'space-between', +    padding: Padding.padding16, +    borderBottomWidth: 1, +    borderBottomColor: Colors.borderLight, +  }, +  settingRowLeft: { +    flexDirection: 'row', +    alignItems: 'center', +    gap: Spacing.medium, +  }, +  settingRowRight: { +    flexDirection: 'row', +    alignItems: 'center', +    gap: Spacing.small, +  }, +  settingLabel: { +    ...Typography.body, +    color: Colors.textPrimary, +  }, +  settingValue: { +    ...Typography.body, +    color: Colors.textSecondary, +  }, +  sliderSetting: { +    padding: Padding.padding16, +    borderBottomWidth: 1, +    borderBottomColor: Colors.borderLight, +  }, +  sliderHeader: { +    flexDirection: 'row', +    justifyContent: 'space-between', +    alignItems: 'center', +    marginBottom: Spacing.medium, +  }, +  sliderValue: { +    ...Typography.body, +    color: Colors.primaryBlue, +    fontWeight: '600', +  }, +  sliderControls: { +    flexDirection: 'row', +    alignItems: 'center', +    gap: Spacing.medium, +  }, +  sliderButton: { +    width: 36, +    height: 36, +    borderRadius: 18, +    backgroundColor: Colors.backgroundSecondary, +    justifyContent: 'center', +    alignItems: 'center', +  }, +  sliderTrack: { +    flex: 1, +    height: 6, +    backgroundColor: Colors.backgroundGray5, +    borderRadius: 3, +  }, +  sliderFill: { +    height: '100%', +    backgroundColor: Colors.primaryBlue, +    borderRadius: 3, +  }, +  storageBar: { +    padding: Padding.padding16, +  }, +  storageBarTrack: { +    height: 8, +    backgroundColor: Colors.backgroundGray5, +    borderRadius: 4, +    overflow: 'hidden', +  }, +  storageBarFill: { +    height: '100%', +    backgroundColor: Colors.primaryBlue, +    borderRadius: 4, +  }, +  storageText: { +    ...Typography.footnote, +    color: Colors.textSecondary, +    marginTop: Spacing.small, +    textAlign: 'center', +  }, +  storageDetails: { +    borderTopWidth: 1, +    borderTopColor: Colors.borderLight, +    padding: Padding.padding16, +    gap: Spacing.small, +  }, +  storageDetailRow: { +    flexDirection: 'row', +    justifyContent: 'space-between', +  }, +  storageDetailLabel: { +    ...Typography.subheadline, +    color: Colors.textSecondary, +  }, +  storageDetailValue: { +    ...Typography.subheadline, +    color: Colors.textPrimary, +  }, +  modelRow: { +    flexDirection: 'row', +    alignItems: 'center', +    justifyContent: 'space-between', +    padding: Padding.padding16, +    borderBottomWidth: 1, +    borderBottomColor: Colors.borderLight, +  }, +  modelInfo: { +    flex: 1, +  }, +  modelName: { +    ...Typography.body, +    color: Colors.textPrimary, +  }, +  modelMeta: { +    flexDirection: 'row', +    alignItems: 'center', +    gap: Spacing.small, +    marginTop: Spacing.xSmall, +  }, +  frameworkBadge: { +    backgroundColor: Colors.badgeBlue, +    paddingHorizontal: Spacing.small, +    paddingVertical: Spacing.xxSmall, +    borderRadius: BorderRadius.small, +  }, +  frameworkBadgeText: { +    ...Typography.caption2, +    color: Colors.primaryBlue, +    fontWeight: '600', +  }, +  modelSize: { +    ...Typography.caption, +    color: Colors.textSecondary, +  }, +  deleteButton: { +    padding: Spacing.small, +  }, +  emptyText: { +    ...Typography.body, +    color: Colors.textSecondary, +    textAlign: 'center', +    padding: Padding.padding24, +  }, +  dangerButton: { +    flexDirection: 'row', +    alignItems: 'center', +    justifyContent: 'center', +    gap: Spacing.smallMedium, +    padding: Padding.padding16, +    borderBottomWidth: 1, +    borderBottomColor: Colors.borderLight, +  }, +  dangerButtonRed: { +    borderBottomWidth: 0, +  }, +  dangerButtonText: { +    ...Typography.body, +    color: Colors.primaryOrange, +  }, +  dangerButtonTextRed: { +    color: Colors.primaryRed, +  }, +  versionContainer: { +    alignItems: 'center', +    padding: Padding.padding24, +    marginTop: Spacing.large, +    marginBottom: Spacing.xxxLarge, +  }, +  versionText: { +    ...Typography.footnote, +    color: Colors.textTertiary, +  }, +  versionSubtext: { +    ...Typography.caption, +    color: Colors.textTertiary, +    marginTop: Spacing.xSmall, +  }, +  // SDK Status styles +  statusRow: { +    flexDirection: 'row', +    justifyContent: 'space-between', +    alignItems: 'center', +    padding: Padding.padding16, +    borderBottomWidth: 1, +    borderBottomColor: Colors.borderLight, +  }, +  statusLabel: { +    ...Typography.body, +    color: Colors.textPrimary, +  }, +  statusValue: { +    ...Typography.body, +    color: Colors.primaryBlue, +    fontWeight: '600', +  }, +  capabilitiesContainer: { +    padding: Padding.padding16, +    borderBottomWidth: 1, +    borderBottomColor: Colors.borderLight, +  }, +  capabilitiesLabel: { +    ...Typography.subheadline, +    color: Colors.textSecondary, +    marginBottom: Spacing.small, +  }, +  capabilitiesList: { +    flexDirection: 'row', +    flexWrap: 'wrap', +    gap: Spacing.small, +  }, +  capabilityBadge: { +    flexDirection: 'row', +    alignItems: 'center', +    gap: Spacing.xSmall, +    backgroundColor: Colors.badgeGreen, +    paddingHorizontal: Spacing.smallMedium, +    paddingVertical: Spacing.xSmall, +    borderRadius: BorderRadius.small, +  }, +  capabilityText: { +    ...Typography.caption, +    color: Colors.primaryGreen, +    fontWeight: '600', +  }, +  noCapabilities: { +    ...Typography.body, +    color: Colors.textTertiary, +    fontStyle: 'italic', +  }, +  modelStatusContainer: { +    padding: Padding.padding16, +  }, +  modelStatusGrid: { +    flexDirection: 'row', +    flexWrap: 'wrap', +    gap: Spacing.small, +    marginTop: Spacing.small, +  }, +  modelStatusItem: { +    flexDirection: 'row', +    alignItems: 'center', +    gap: Spacing.xSmall, +    backgroundColor: Colors.backgroundSecondary, +    paddingHorizontal: Spacing.medium, +    paddingVertical: Spacing.small, +    borderRadius: BorderRadius.small, +  }, +  modelStatusItemLoaded: { +    backgroundColor: Colors.badgeGreen, +  }, +  modelStatusText: { +    ...Typography.footnote, +    color: Colors.textSecondary, +    fontWeight: '600', +  }, +  // Model catalog styles +  catalogModelRow: { +    flexDirection: 'row', +    alignItems: 'center', +    justifyContent: 'space-between', +    padding: Padding.padding16, +    borderBottomWidth: 1, +    borderBottomColor: Colors.borderLight, +  }, +  catalogModelInfo: { +    flex: 1, +    marginRight: Spacing.medium, +  }, +  catalogModelHeader: { +    flexDirection: 'row', +    alignItems: 'center', +    gap: Spacing.small, +    marginBottom: Spacing.xSmall, +  }, +  catalogModelName: { +    ...Typography.body, +    color: Colors.textPrimary, +    fontWeight: '600', +  }, +  catalogModelBadge: { +    backgroundColor: Colors.badgeBlue, +    paddingHorizontal: Spacing.small, +    paddingVertical: Spacing.xxSmall, +    borderRadius: BorderRadius.small, +  }, +  catalogModelBadgeText: { +    ...Typography.caption2, +    color: Colors.primaryBlue, +    fontWeight: '600', +    textTransform: 'uppercase', +  }, +  catalogModelDescription: { +    ...Typography.footnote, +    color: Colors.textSecondary, +    marginBottom: Spacing.xSmall, +  }, +  catalogModelMeta: { +    flexDirection: 'row', +    alignItems: 'center', +    gap: Spacing.small, +  }, +  catalogModelSize: { +    ...Typography.caption, +    color: Colors.textTertiary, +  }, +  catalogModelFormat: { +    ...Typography.caption, +    color: Colors.textTertiary, +  }, +  catalogModelButton: { +    width: 44, +    height: 44, +    borderRadius: 22, +    backgroundColor: Colors.backgroundSecondary, +    justifyContent: 'center', +    alignItems: 'center', +  }, +  catalogModelButtonDownloaded: { +    backgroundColor: Colors.badgeGreen, +  }, +  catalogModelButtonDownloading: { +    backgroundColor: Colors.badgeOrange, +  }, +  downloadProgressContainer: { +    flexDirection: 'row', +    alignItems: 'center', +    gap: Spacing.small, +    marginTop: Spacing.small, +  }, +  downloadProgressTrack: { +    flex: 1, +    height: 4, +    backgroundColor: Colors.backgroundGray5, +    borderRadius: 2, +    overflow: 'hidden', +  }, +  downloadProgressFill: { +    height: '100%', +    backgroundColor: Colors.primaryBlue, +    borderRadius: 2, +  }, +  downloadProgressText: { +    ...Typography.caption, +    color: Colors.primaryBlue, +    fontWeight: '600', +    minWidth: 40, +    textAlign: 'right', +  }, +  // API Configuration styles +  apiConfigRow: { +    flexDirection: 'row', +    justifyContent: 'space-between', +    alignItems: 'center', +    padding: Padding.padding16, +  }, +  apiConfigLabel: { +    ...Typography.body, +    color: Colors.textPrimary, +  }, +  apiConfigValue: { +    ...Typography.body, +    fontWeight: '500', +  }, +  apiConfigDivider: { +    height: 1, +    backgroundColor: Colors.borderLight, +    marginHorizontal: Padding.padding16, +  }, +  apiConfigButtons: { +    flexDirection: 'row', +    padding: Padding.padding16, +    gap: Spacing.small, +  }, +  apiConfigButton: { +    paddingHorizontal: Padding.padding16, +    paddingVertical: Spacing.small, +    borderRadius: BorderRadius.small, +    borderWidth: 1, +    borderColor: Colors.primaryBlue, +  }, +  apiConfigButtonClear: { +    borderColor: Colors.primaryRed, +  }, +  apiConfigButtonText: { +    ...Typography.subheadline, +    color: Colors.primaryBlue, +    fontWeight: '600', +  }, +  apiConfigButtonTextClear: { +    color: Colors.primaryRed, +  }, +  apiConfigHint: { +    ...Typography.footnote, +    color: Colors.textSecondary, +    paddingHorizontal: Padding.padding16, +    paddingBottom: Padding.padding16, +  }, +  // Modal styles +  modalOverlay: { +    flex: 1, +    backgroundColor: 'rgba(0, 0, 0, 0.5)', +    justifyContent: 'center', +    alignItems: 'center', +    padding: Padding.padding24, +  }, +  modalContent: { +    backgroundColor: Colors.backgroundPrimary, +    borderRadius: BorderRadius.large, +    padding: Padding.padding24, +    width: '100%', +    maxWidth: 400, +  }, +  modalTitle: { +    ...Typography.title2, +    color: Colors.textPrimary, +    marginBottom: Spacing.large, +    textAlign: 'center', +  }, +  inputGroup: { +    marginBottom: Spacing.large, +  }, +  inputLabel: { +    ...Typography.subheadline, +    color: Colors.textSecondary, +    marginBottom: Spacing.small, +  }, +  input: { +    backgroundColor: Colors.backgroundSecondary, +    borderRadius: BorderRadius.small, +    padding: Padding.padding12, +    ...Typography.body, +    color: Colors.textPrimary, +    borderWidth: 1, +    borderColor: Colors.borderLight, +  }, +  passwordInputContainer: { +    flexDirection: 'row', +    alignItems: 'center', +    backgroundColor: Colors.backgroundSecondary, +    borderRadius: BorderRadius.small, +    borderWidth: 1, +    borderColor: Colors.borderLight, +  }, +  passwordInput: { +    flex: 1, +    padding: Padding.padding12, +    ...Typography.body, +    color: Colors.textPrimary, +  }, +  passwordToggle: { +    padding: Padding.padding12, +  }, +  inputHint: { +    ...Typography.caption, +    color: Colors.textTertiary, +    marginTop: Spacing.xSmall, +  }, +  warningBox: { +    flexDirection: 'row', +    backgroundColor: Colors.badgeOrange, +    borderRadius: BorderRadius.small, +    padding: Padding.padding12, +    gap: Spacing.small, +    marginBottom: Spacing.large, +  }, +  warningText: { +    ...Typography.footnote, +    color: Colors.textSecondary, +    flex: 1, +  }, +  modalButtons: { +    flexDirection: 'row', +    justifyContent: 'flex-end', +    gap: Spacing.medium, +  }, +  modalButton: { +    paddingHorizontal: Padding.padding16, +    paddingVertical: Spacing.smallMedium, +    borderRadius: BorderRadius.small, +    minWidth: 80, +    alignItems: 'center', +  }, +  modalButtonCancel: { +    backgroundColor: 'transparent', +  }, +  modalButtonSave: { +    backgroundColor: Colors.primaryBlue, +  }, +  modalButtonDisabled: { +    backgroundColor: Colors.backgroundGray5, +  }, +  modalButtonTextCancel: { +    ...Typography.body, +    color: Colors.textSecondary, +  }, +  modalButtonTextSave: { +    ...Typography.body, +    color: Colors.textWhite, +    fontWeight: '600', +  }, +  modalButtonTextDisabled: { +    color: Colors.textTertiary, +  }, +  // System Prompt styles +  systemPromptContainer: { +    padding: Padding.padding16, +    borderBottomWidth: 1, +    borderBottomColor: Colors.borderLight, +  }, +  systemPromptLabel: { +    ...Typography.subheadline, +    color: Colors.textPrimary, +    marginBottom: Spacing.small, +  }, +  systemPromptInput: { +    backgroundColor: Colors.backgroundSecondary, +    borderRadius: BorderRadius.small, +    padding: Padding.padding12, +    ...Typography.body, +    color: Colors.textPrimary, +    borderWidth: 1, +    borderColor: Colors.borderLight, +    minHeight: 80, +  }, +  saveSettingsButton: { +    flexDirection: 'row', +    alignItems: 'center', +    justifyContent: 'center', +    gap: Spacing.small, +    backgroundColor: Colors.primaryBlue, +    padding: Padding.padding16, +    margin: Padding.padding16, +    borderRadius: BorderRadius.small, +  }, +  saveSettingsButtonText: { +    ...Typography.body, +    color: Colors.textWhite, +    fontWeight: '600', +  }, +  // Tool Settings styles +  toolSettingRow: { +    flexDirection: 'row', +    justifyContent: 'space-between', +    alignItems: 'center', +    padding: Padding.padding16, +  }, +  toolSettingInfo: { +    flex: 1, +    marginRight: Spacing.medium, +  }, +  toolSettingLabel: { +    ...Typography.body, +    color: Colors.textPrimary, +    fontWeight: '500', +  }, +  toolSettingDescription: { +    ...Typography.footnote, +    color: Colors.textSecondary, +    marginTop: Spacing.xxSmall, +  }, +  toolSettingValue: { +    ...Typography.body, +    fontWeight: '600', +  }, +  toggleButton: { +    width: 51, +    height: 31, +    borderRadius: 15.5, +    backgroundColor: Colors.backgroundGray5, +    padding: 2, +    justifyContent: 'center', +  }, +  toggleButtonActive: { +    backgroundColor: Colors.primaryBlue, +  }, +  toggleKnob: { +    width: 27, +    height: 27, +    borderRadius: 13.5, +    backgroundColor: Colors.textWhite, +    shadowColor: '#000', +    shadowOffset: { width: 0, height: 2 }, +    shadowOpacity: 0.2, +    shadowRadius: 2, +    elevation: 2, +  }, +  toggleKnobActive: { +    alignSelf: 'flex-end', +  }, +  demoToolsButton: { +    flexDirection: 'row', +    alignItems: 'center', +    justifyContent: 'center', +    gap: Spacing.small, +    padding: Padding.padding16, +  }, +  demoToolsButtonText: { +    ...Typography.body, +    color: Colors.primaryBlue, +    fontWeight: '600', +  }, +  toolRow: { +    flexDirection: 'row', +    alignItems: 'flex-start', +    padding: Padding.padding16, +    gap: Spacing.medium, +  }, +  toolInfo: { +    flex: 1, +  }, +  toolName: { +    ...Typography.subheadline, +    color: Colors.textPrimary, +    fontWeight: '600', +  }, +  toolDescription: { +    ...Typography.footnote, +    color: Colors.textSecondary, +    marginTop: Spacing.xxSmall, +  }, +  toolParams: { +    flexDirection: 'row', +    flexWrap: 'wrap', +    gap: Spacing.xSmall, +    marginTop: Spacing.small, +  }, +  toolParamChip: { +    backgroundColor: Colors.badgeBlue, +    paddingHorizontal: Spacing.small, +    paddingVertical: Spacing.xxSmall, +    borderRadius: BorderRadius.small, +  }, +  toolParamText: { +    ...Typography.caption2, +    color: Colors.primaryBlue, +    fontWeight: '500', +  }, +  clearToolsButton: { +    flexDirection: 'row', +    alignItems: 'center', +    justifyContent: 'center', +    gap: Spacing.small, +    padding: Padding.padding16, +  }, +  clearToolsButtonText: { +    ...Typography.body, +    color: Colors.statusRed, +    fontWeight: '600', +  }, }); -export default SettingsScreen; +export default SettingsScreen; \ No newline at end of file diff --git a/examples/react-native/RunAnywhereAI/src/screens/TTSScreen.tsx b/examples/react-native/RunAnywhereAI/src/screens/TTSScreen.tsx index 2b98d970b..5e18afc8d 100644 --- a/examples/react-native/RunAnywhereAI/src/screens/TTSScreen.tsx +++ b/examples/react-native/RunAnywhereAI/src/screens/TTSScreen.tsx @@ -1012,7 +1012,8 @@ export const TTSScreen: React.FC = () => { Voice Settings {renderSlider('Speed', speed, setSpeed)} - {renderSlider('Pitch', pitch, setPitch)} + {/* Pitch slider - Commented out for now as it is not implemented in the current TTS models */} + {/* {renderSlider('Pitch', pitch, setPitch)} */} {renderSlider( 'Volume', volume, diff --git a/examples/react-native/RunAnywhereAI/src/types/settings.ts b/examples/react-native/RunAnywhereAI/src/types/settings.ts index ac95be54a..e51a13b4c 100644 --- a/examples/react-native/RunAnywhereAI/src/types/settings.ts +++ b/examples/react-native/RunAnywhereAI/src/types/settings.ts @@ -123,3 +123,13 @@ export const RoutingPolicyDescriptions: Record = { [RoutingPolicy.PreferCloud]: 'Prefer cloud execution, fall back to device if offline.', }; + +/** + * AsyncStorage keys for generation settings persistence + * Matches iOS/Android naming convention for cross-platform consistency + */ +export const GENERATION_SETTINGS_KEYS = { + TEMPERATURE: 'defaultTemperature', + MAX_TOKENS: 'defaultMaxTokens', + SYSTEM_PROMPT: 'defaultSystemPrompt', +} as const; diff --git a/sdk/runanywhere-commons/src/backends/llamacpp/llamacpp_backend.cpp b/sdk/runanywhere-commons/src/backends/llamacpp/llamacpp_backend.cpp index 31f19581f..d9b844135 100644 --- a/sdk/runanywhere-commons/src/backends/llamacpp/llamacpp_backend.cpp +++ b/sdk/runanywhere-commons/src/backends/llamacpp/llamacpp_backend.cpp @@ -190,18 +190,6 @@ bool LlamaCppTextGeneration::load_model(const std::string& model_path, if (config.contains("max_context_size")) { max_default_context_ = config["max_context_size"].get(); } - if (config.contains("temperature")) { - temperature_ = config["temperature"].get(); - } - if (config.contains("min_p")) { - min_p_ = config["min_p"].get(); - } - if (config.contains("top_p")) { - top_p_ = config["top_p"].get(); - } - if (config.contains("top_k")) { - top_k_ = config["top_k"].get(); - } model_config_ = config; model_path_ = model_path; @@ -340,29 +328,15 @@ bool LlamaCppTextGeneration::load_model(const std::string& model_path, return false; } + // Note: Sampler chain is rebuilt per-request in generate_stream() using request parameters + // This initial sampler is not used for actual generation auto sparams = llama_sampler_chain_default_params(); sparams.no_perf = true; sampler_ = llama_sampler_chain_init(sparams); - - if (temperature_ > 0.0f) { - llama_sampler_chain_add(sampler_, llama_sampler_init_penalties(64, 1.2f, 0.0f, 0.0f)); - - if (top_k_ > 0) { - llama_sampler_chain_add(sampler_, llama_sampler_init_top_k(top_k_)); - } - - llama_sampler_chain_add(sampler_, llama_sampler_init_top_p(top_p_, 1)); - llama_sampler_chain_add(sampler_, llama_sampler_init_temp(temperature_)); - llama_sampler_chain_add(sampler_, llama_sampler_init_dist(LLAMA_DEFAULT_SEED)); - } else { - llama_sampler_chain_add(sampler_, llama_sampler_init_greedy()); - } - - LOGI("Sampler chain: penalties(64,1.2) -> top_k(%d) -> top_p(%.2f) -> temp(%.2f) -> dist", - top_k_, top_p_, temperature_); + llama_sampler_chain_add(sampler_, llama_sampler_init_greedy()); model_loaded_ = true; - LOGI("Model loaded successfully: context_size=%d, temp=%.2f", context_size_, temperature_); + LOGI("Model loaded successfully: context_size=%d", context_size_); return true; } @@ -571,7 +545,41 @@ bool LlamaCppTextGeneration::generate_stream(const TextGenerationRequest& reques return false; } - llama_sampler_reset(sampler_); + // Configure sampler with request parameters + if (sampler_) { + llama_sampler_free(sampler_); + } + + auto sparams = llama_sampler_chain_default_params(); + sparams.no_perf = true; + sampler_ = llama_sampler_chain_init(sparams); + + if (request.temperature > 0.0f) { + // Use default penalties (1.2f repetition) or request params if added later + llama_sampler_chain_add(sampler_, + llama_sampler_init_penalties(64, request.repetition_penalty, 0.0f, 0.0f)); + + if (request.top_k > 0) { + llama_sampler_chain_add(sampler_, llama_sampler_init_top_k(request.top_k)); + } + + llama_sampler_chain_add(sampler_, llama_sampler_init_top_p(request.top_p, 1)); + llama_sampler_chain_add(sampler_, llama_sampler_init_temp(request.temperature)); + llama_sampler_chain_add(sampler_, llama_sampler_init_dist(LLAMA_DEFAULT_SEED)); + } else { + llama_sampler_chain_add(sampler_, llama_sampler_init_greedy()); + } + + // Log generation parameters + LOGI("[PARAMS] LLM generate_stream (per-request options): temperature=%.4f, top_p=%.4f, top_k=%d, " + "max_tokens=%d (effective=%d), repetition_penalty=%.4f, " + "system_prompt_len=%zu", + request.temperature, request.top_p, request.top_k, + request.max_tokens, effective_max_tokens, request.repetition_penalty, + request.system_prompt.length()); + + // No need to reset as we just created it + // llama_sampler_reset(sampler_); const auto vocab = llama_model_get_vocab(model_); @@ -696,10 +704,6 @@ nlohmann::json LlamaCppTextGeneration::get_model_info() const { info["context_size"] = context_size_; info["model_training_context"] = llama_model_n_ctx_train(model_); info["max_default_context"] = max_default_context_; - info["temperature"] = temperature_; - info["top_k"] = top_k_; - info["top_p"] = top_p_; - info["min_p"] = min_p_; char buf[256]; if (llama_model_meta_val_str(model_, "general.name", buf, sizeof(buf)) > 0) { diff --git a/sdk/runanywhere-commons/src/backends/llamacpp/llamacpp_backend.h b/sdk/runanywhere-commons/src/backends/llamacpp/llamacpp_backend.h index 2d8deb065..01a54c535 100644 --- a/sdk/runanywhere-commons/src/backends/llamacpp/llamacpp_backend.h +++ b/sdk/runanywhere-commons/src/backends/llamacpp/llamacpp_backend.h @@ -140,11 +140,6 @@ class LlamaCppTextGeneration { int context_size_ = 0; int max_default_context_ = 8192; - float temperature_ = 0.8f; - float top_p_ = 0.95f; - float min_p_ = 0.05f; - int top_k_ = 40; - mutable std::mutex mutex_; }; diff --git a/sdk/runanywhere-commons/src/backends/llamacpp/rac_llm_llamacpp.cpp b/sdk/runanywhere-commons/src/backends/llamacpp/rac_llm_llamacpp.cpp index cde9dc275..09ade4967 100644 --- a/sdk/runanywhere-commons/src/backends/llamacpp/rac_llm_llamacpp.cpp +++ b/sdk/runanywhere-commons/src/backends/llamacpp/rac_llm_llamacpp.cpp @@ -16,8 +16,12 @@ #include "llamacpp_backend.h" #include "rac/core/rac_error.h" +#include "rac/core/rac_logger.h" #include "rac/infrastructure/events/rac_events.h" +// Use the RAC logging system +#define LOGI(...) RAC_LOG_INFO("LLM.LlamaCpp.C-API", __VA_ARGS__) + // ============================================================================= // INTERNAL HANDLE STRUCTURE // ============================================================================= @@ -152,6 +156,9 @@ rac_result_t rac_llm_llamacpp_generate(rac_handle_t handle, const char* prompt, request.max_tokens = options->max_tokens; request.temperature = options->temperature; request.top_p = options->top_p; + if (options->system_prompt != nullptr) { + request.system_prompt = options->system_prompt; + } // Handle stop sequences if available if (options->stop_sequences != nullptr && options->num_stop_sequences > 0) { for (int32_t i = 0; i < options->num_stop_sequences; i++) { @@ -160,6 +167,14 @@ rac_result_t rac_llm_llamacpp_generate(rac_handle_t handle, const char* prompt, } } } + LOGI("[PARAMS] LLM C-API (from caller options): max_tokens=%d, temperature=%.4f, " + "top_p=%.4f, system_prompt=%s", + request.max_tokens, request.temperature, request.top_p, + request.system_prompt.empty() ? "(none)" : "(set)"); + } else { + LOGI("[PARAMS] LLM C-API (using struct defaults): max_tokens=%d, temperature=%.4f, " + "top_p=%.4f, system_prompt=(none)", + request.max_tokens, request.temperature, request.top_p); } // Generate using C++ class @@ -203,6 +218,9 @@ rac_result_t rac_llm_llamacpp_generate_stream(rac_handle_t handle, const char* p request.max_tokens = options->max_tokens; request.temperature = options->temperature; request.top_p = options->top_p; + if (options->system_prompt != nullptr) { + request.system_prompt = options->system_prompt; + } if (options->stop_sequences != nullptr && options->num_stop_sequences > 0) { for (int32_t i = 0; i < options->num_stop_sequences; i++) { if (options->stop_sequences[i]) { @@ -210,6 +228,14 @@ rac_result_t rac_llm_llamacpp_generate_stream(rac_handle_t handle, const char* p } } } + LOGI("[PARAMS] LLM C-API (from caller options): max_tokens=%d, temperature=%.4f, " + "top_p=%.4f, system_prompt=%s", + request.max_tokens, request.temperature, request.top_p, + request.system_prompt.empty() ? "(none)" : "(set)"); + } else { + LOGI("[PARAMS] LLM C-API (using struct defaults): max_tokens=%d, temperature=%.4f, " + "top_p=%.4f, system_prompt=(none)", + request.max_tokens, request.temperature, request.top_p); } // Stream using C++ class diff --git a/sdk/runanywhere-commons/src/jni/CMakeLists.txt b/sdk/runanywhere-commons/src/jni/CMakeLists.txt index 496b7e9ab..c193d6792 100644 --- a/sdk/runanywhere-commons/src/jni/CMakeLists.txt +++ b/sdk/runanywhere-commons/src/jni/CMakeLists.txt @@ -22,6 +22,22 @@ project(runanywhere_commons_jni) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) +include(FetchContent) + +# needed for string parsing +if(NOT DEFINED NLOHMANN_JSON_VERSION) + set(NLOHMANN_JSON_VERSION "3.11.3") +endif() + +FetchContent_Declare( + nlohmann_json + GIT_REPOSITORY https://github.com/nlohmann/json.git + GIT_TAG v${NLOHMANN_JSON_VERSION} + GIT_SHALLOW TRUE +) +set(JSON_BuildTests OFF CACHE BOOL "" FORCE) +FetchContent_MakeAvailable(nlohmann_json) + # Find JNI find_package(JNI REQUIRED) include_directories(${JNI_INCLUDE_DIRS}) @@ -41,6 +57,7 @@ add_library(runanywhere_commons_jni SHARED ${JNI_SOURCES}) # Backend libraries are NOT linked here - they have their own JNI libraries target_link_libraries(runanywhere_commons_jni rac_commons + nlohmann_json::nlohmann_json ${JNI_LIBRARIES} ) diff --git a/sdk/runanywhere-commons/src/jni/runanywhere_commons_jni.cpp b/sdk/runanywhere-commons/src/jni/runanywhere_commons_jni.cpp index f98861980..12ff21e93 100644 --- a/sdk/runanywhere-commons/src/jni/runanywhere_commons_jni.cpp +++ b/sdk/runanywhere-commons/src/jni/runanywhere_commons_jni.cpp @@ -20,6 +20,7 @@ #include #include #include +#include // Include runanywhere-commons C API headers #include "rac/core/rac_analytics_events.h" @@ -532,6 +533,28 @@ Java_com_runanywhere_sdk_native_bridge_RunAnywhereBridge_racLlmComponentGenerate options.temperature = 0.7f; options.top_p = 1.0f; options.streaming_enabled = RAC_FALSE; + options.system_prompt = RAC_NULL; + + // Parse configJson if provided + std::string sys_prompt_storage; + if (config != nullptr) { + try { + auto j = nlohmann::json::parse(config); + options.max_tokens = j.value("max_tokens", 512); + options.temperature = j.value("temperature", 0.7f); + options.top_p = j.value("top_p", 1.0f); + sys_prompt_storage = j.value("system_prompt", std::string("")); + if (!sys_prompt_storage.empty()) { + options.system_prompt = sys_prompt_storage.c_str(); + } + } catch (const nlohmann::json::exception& e) { + LOGe("Failed to parse LLM config JSON: %s", e.what()); + } + } + + LOGi("racLlmComponentGenerate options: temp=%.2f, max_tokens=%d, top_p=%.2f, system_prompt=%s", + options.temperature, options.max_tokens, options.top_p, + options.system_prompt ? "(set)" : "(none)"); rac_llm_result_t result = {}; LOGi("racLlmComponentGenerate calling rac_llm_component_generate..."); @@ -551,39 +574,14 @@ Java_com_runanywhere_sdk_native_bridge_RunAnywhereBridge_racLlmComponentGenerate LOGi("racLlmComponentGenerate result text length=%zu", strlen(result.text)); // Build JSON result - keys must match what Kotlin expects - std::string json = "{"; - json += "\"text\":\""; - // Escape special characters in text for JSON - for (const char* p = result.text; *p; p++) { - switch (*p) { - case '"': - json += "\\\""; - break; - case '\\': - json += "\\\\"; - break; - case '\n': - json += "\\n"; - break; - case '\r': - json += "\\r"; - break; - case '\t': - json += "\\t"; - break; - default: - json += *p; - break; - } - } - json += "\","; - // Kotlin expects these keys: - json += "\"tokens_generated\":" + std::to_string(result.completion_tokens) + ","; - json += "\"tokens_evaluated\":" + std::to_string(result.prompt_tokens) + ","; - json += "\"stop_reason\":" + std::to_string(0) + ","; // 0 = normal completion - json += "\"total_time_ms\":" + std::to_string(result.total_time_ms) + ","; - json += "\"tokens_per_second\":" + std::to_string(result.tokens_per_second); - json += "}"; + nlohmann::json json_obj; + json_obj["text"] = std::string(result.text); + json_obj["tokens_generated"] = result.completion_tokens; + json_obj["tokens_evaluated"] = result.prompt_tokens; + json_obj["stop_reason"] = 0; // 0 = normal completion + json_obj["total_time_ms"] = result.total_time_ms; + json_obj["tokens_per_second"] = result.tokens_per_second; + std::string json = json_obj.dump(); LOGi("racLlmComponentGenerate returning JSON: %zu bytes", json.length()); @@ -808,6 +806,28 @@ Java_com_runanywhere_sdk_native_bridge_RunAnywhereBridge_racLlmComponentGenerate options.temperature = 0.7f; options.top_p = 1.0f; options.streaming_enabled = RAC_TRUE; + options.system_prompt = RAC_NULL; + + // Parse configJson if provided + std::string sys_prompt_storage; + if (config != nullptr) { + try { + auto j = nlohmann::json::parse(config); + options.max_tokens = j.value("max_tokens", 512); + options.temperature = j.value("temperature", 0.7f); + options.top_p = j.value("top_p", 1.0f); + sys_prompt_storage = j.value("system_prompt", std::string("")); + if (!sys_prompt_storage.empty()) { + options.system_prompt = sys_prompt_storage.c_str(); + } + } catch (const nlohmann::json::exception& e) { + LOGe("Failed to parse LLM config JSON: %s", e.what()); + } + } + + LOGi("racLlmComponentGenerateStream options: temp=%.2f, max_tokens=%d, top_p=%.2f, system_prompt=%s", + options.temperature, options.max_tokens, options.top_p, + options.system_prompt ? "(set)" : "(none)"); // Create streaming context LLMStreamContext ctx; @@ -838,39 +858,14 @@ Java_com_runanywhere_sdk_native_bridge_RunAnywhereBridge_racLlmComponentGenerate ctx.accumulated_text.length(), ctx.token_count); // Build JSON result - keys must match what Kotlin expects - std::string json = "{"; - json += "\"text\":\""; - // Escape special characters in text for JSON - for (char c : ctx.accumulated_text) { - switch (c) { - case '"': - json += "\\\""; - break; - case '\\': - json += "\\\\"; - break; - case '\n': - json += "\\n"; - break; - case '\r': - json += "\\r"; - break; - case '\t': - json += "\\t"; - break; - default: - json += c; - break; - } - } - json += "\","; - // Kotlin expects these keys: - json += "\"tokens_generated\":" + std::to_string(ctx.final_result.completion_tokens) + ","; - json += "\"tokens_evaluated\":" + std::to_string(ctx.final_result.prompt_tokens) + ","; - json += "\"stop_reason\":" + std::to_string(0) + ","; // 0 = normal completion - json += "\"total_time_ms\":" + std::to_string(ctx.final_result.total_time_ms) + ","; - json += "\"tokens_per_second\":" + std::to_string(ctx.final_result.tokens_per_second); - json += "}"; + nlohmann::json json_obj; + json_obj["text"] = ctx.accumulated_text; + json_obj["tokens_generated"] = ctx.final_result.completion_tokens; + json_obj["tokens_evaluated"] = ctx.final_result.prompt_tokens; + json_obj["stop_reason"] = 0; // 0 = normal completion + json_obj["total_time_ms"] = ctx.final_result.total_time_ms; + json_obj["tokens_per_second"] = ctx.final_result.tokens_per_second; + std::string json = json_obj.dump(); LOGi("racLlmComponentGenerateStream returning JSON: %zu bytes", json.length()); @@ -924,6 +919,28 @@ Java_com_runanywhere_sdk_native_bridge_RunAnywhereBridge_racLlmComponentGenerate options.temperature = 0.7f; options.top_p = 1.0f; options.streaming_enabled = RAC_TRUE; + options.system_prompt = RAC_NULL; + + // Parse configJson if provided + std::string sys_prompt_storage; + if (config != nullptr) { + try { + auto j = nlohmann::json::parse(config); + options.max_tokens = j.value("max_tokens", 512); + options.temperature = j.value("temperature", 0.7f); + options.top_p = j.value("top_p", 1.0f); + sys_prompt_storage = j.value("system_prompt", std::string("")); + if (!sys_prompt_storage.empty()) { + options.system_prompt = sys_prompt_storage.c_str(); + } + } catch (const nlohmann::json::exception& e) { + LOGe("Failed to parse LLM config JSON: %s", e.what()); + } + } + + LOGi("racLlmComponentGenerateStreamWithCallback options: temp=%.2f, max_tokens=%d, top_p=%.2f, system_prompt=%s", + options.temperature, options.max_tokens, options.top_p, + options.system_prompt ? "(set)" : "(none)"); // Create streaming callback context LLMStreamCallbackContext ctx; @@ -954,37 +971,14 @@ Java_com_runanywhere_sdk_native_bridge_RunAnywhereBridge_racLlmComponentGenerate ctx.accumulated_text.length(), ctx.token_count); // Build JSON result - std::string json = "{"; - json += "\"text\":\""; - for (char c : ctx.accumulated_text) { - switch (c) { - case '"': - json += "\\\""; - break; - case '\\': - json += "\\\\"; - break; - case '\n': - json += "\\n"; - break; - case '\r': - json += "\\r"; - break; - case '\t': - json += "\\t"; - break; - default: - json += c; - break; - } - } - json += "\","; - json += "\"tokens_generated\":" + std::to_string(ctx.final_result.completion_tokens) + ","; - json += "\"tokens_evaluated\":" + std::to_string(ctx.final_result.prompt_tokens) + ","; - json += "\"stop_reason\":" + std::to_string(0) + ","; - json += "\"total_time_ms\":" + std::to_string(ctx.final_result.total_time_ms) + ","; - json += "\"tokens_per_second\":" + std::to_string(ctx.final_result.tokens_per_second); - json += "}"; + nlohmann::json json_obj; + json_obj["text"] = ctx.accumulated_text; + json_obj["tokens_generated"] = ctx.final_result.completion_tokens; + json_obj["tokens_evaluated"] = ctx.final_result.prompt_tokens; + json_obj["stop_reason"] = 0; + json_obj["total_time_ms"] = ctx.final_result.total_time_ms; + json_obj["tokens_per_second"] = ctx.final_result.tokens_per_second; + std::string json = json_obj.dump(); LOGi("racLlmComponentGenerateStreamWithCallback returning JSON: %zu bytes", json.length()); @@ -1135,20 +1129,21 @@ Java_com_runanywhere_sdk_native_bridge_RunAnywhereBridge_racSttComponentTranscri // Parse configJson to override sample_rate if provided if (configJson != nullptr) { - const char* json = env->GetStringUTFChars(configJson, nullptr); - if (json != nullptr) { - // Simple JSON parsing for sample_rate - const char* sample_rate_key = "\"sample_rate\":"; - const char* pos = strstr(json, sample_rate_key); - if (pos != nullptr) { - pos += strlen(sample_rate_key); - int sample_rate = atoi(pos); - if (sample_rate > 0) { - options.sample_rate = sample_rate; - LOGd("Using sample_rate from config: %d", sample_rate); + const char* json_str = env->GetStringUTFChars(configJson, nullptr); + if (json_str != nullptr) { + try { + auto json = nlohmann::json::parse(json_str); + if (json.contains("sample_rate") && json["sample_rate"].is_number()) { + int sample_rate = json["sample_rate"].get(); + if (sample_rate > 0) { + options.sample_rate = sample_rate; + LOGd("Using sample_rate from config: %d", sample_rate); + } } + } catch (const nlohmann::json::exception& e) { + LOGe("Failed to parse STT config JSON: %s", e.what()); } - env->ReleaseStringUTFChars(configJson, json); + env->ReleaseStringUTFChars(configJson, json_str); } } @@ -1171,40 +1166,13 @@ Java_com_runanywhere_sdk_native_bridge_RunAnywhereBridge_racSttComponentTranscri } // Build JSON result - std::string json_result = "{"; - json_result += "\"text\":\""; - if (result.text != nullptr) { - // Escape special characters in text - for (const char* p = result.text; *p; ++p) { - switch (*p) { - case '"': - json_result += "\\\""; - break; - case '\\': - json_result += "\\\\"; - break; - case '\n': - json_result += "\\n"; - break; - case '\r': - json_result += "\\r"; - break; - case '\t': - json_result += "\\t"; - break; - default: - json_result += *p; - break; - } - } - } - json_result += "\","; - json_result += "\"language\":\"" + - std::string(result.detected_language ? result.detected_language : "en") + "\","; - json_result += "\"duration_ms\":" + std::to_string(result.processing_time_ms) + ","; - json_result += "\"completion_reason\":1,"; // END_OF_AUDIO - json_result += "\"confidence\":" + std::to_string(result.confidence); - json_result += "}"; + nlohmann::json json_obj; + json_obj["text"] = result.text ? std::string(result.text) : ""; + json_obj["language"] = result.detected_language ? std::string(result.detected_language) : "en"; + json_obj["duration_ms"] = result.processing_time_ms; + json_obj["completion_reason"] = 1; // END_OF_AUDIO + json_obj["confidence"] = result.confidence; + std::string json_result = json_obj.dump(); rac_stt_result_free(&result); @@ -1679,25 +1647,19 @@ static std::string modelInfoToJson(const rac_model_info_t* model) { if (!model) return "null"; - std::string json = "{"; - json += "\"model_id\":\"" + std::string(model->id ? model->id : "") + "\","; - json += "\"name\":\"" + std::string(model->name ? model->name : "") + "\","; - json += "\"category\":" + std::to_string(static_cast(model->category)) + ","; - json += "\"format\":" + std::to_string(static_cast(model->format)) + ","; - json += "\"framework\":" + std::to_string(static_cast(model->framework)) + ","; - json += "\"download_url\":" + - (model->download_url ? ("\"" + std::string(model->download_url) + "\"") : "null") + ","; - json += "\"local_path\":" + - (model->local_path ? ("\"" + std::string(model->local_path) + "\"") : "null") + ","; - json += "\"download_size\":" + std::to_string(model->download_size) + ","; - json += "\"context_length\":" + std::to_string(model->context_length) + ","; - json += - "\"supports_thinking\":" + std::string(model->supports_thinking ? "true" : "false") + ","; - json += "\"description\":" + - (model->description ? ("\"" + std::string(model->description) + "\"") : "null"); - json += "}"; - - return json; + nlohmann::json j; + j["model_id"] = model->id ? model->id : ""; + j["name"] = model->name ? model->name : ""; + j["category"] = static_cast(model->category); + j["format"] = static_cast(model->format); + j["framework"] = static_cast(model->framework); + j["download_url"] = model->download_url ? nlohmann::json(model->download_url) : nlohmann::json(nullptr); + j["local_path"] = model->local_path ? nlohmann::json(model->local_path) : nlohmann::json(nullptr); + j["download_size"] = model->download_size; + j["context_length"] = model->context_length; + j["supports_thinking"] = static_cast(model->supports_thinking); + j["description"] = model->description ? nlohmann::json(model->description) : nlohmann::json(nullptr); + return j.dump(); } JNIEXPORT jint JNICALL @@ -2084,25 +2046,22 @@ Java_com_runanywhere_sdk_native_bridge_RunAnywhereBridge_racModelAssignmentFetch } // Build JSON array of models - std::string json = "["; + nlohmann::json json_array = nlohmann::json::array(); for (size_t i = 0; i < count; i++) { - if (i > 0) json += ","; - rac_model_info_t* m = models[i]; - json += "{"; - json += "\"id\":\"" + std::string(m->id ? m->id : "") + "\","; - json += "\"name\":\"" + std::string(m->name ? m->name : "") + "\","; - json += "\"category\":" + std::to_string(m->category) + ","; - json += "\"format\":" + std::to_string(m->format) + ","; - json += "\"framework\":" + std::to_string(m->framework) + ","; - json += "\"downloadUrl\":\"" + std::string(m->download_url ? m->download_url : "") + "\","; - json += "\"downloadSize\":" + std::to_string(m->download_size) + ","; - json += "\"contextLength\":" + std::to_string(m->context_length) + ","; - json += - "\"supportsThinking\":" + std::string(m->supports_thinking == RAC_TRUE ? "true" : "false"); - json += "}"; - } - json += "]"; + nlohmann::json obj; + obj["id"] = m->id ? m->id : ""; + obj["name"] = m->name ? m->name : ""; + obj["category"] = static_cast(m->category); + obj["format"] = static_cast(m->format); + obj["framework"] = static_cast(m->framework); + obj["downloadUrl"] = m->download_url ? m->download_url : ""; + obj["downloadSize"] = m->download_size; + obj["contextLength"] = m->context_length; + obj["supportsThinking"] = static_cast(m->supports_thinking == RAC_TRUE); + json_array.push_back(obj); + } + std::string json = json_array.dump(); // Free models array if (models) { @@ -2268,70 +2227,6 @@ static rac_result_t jni_device_http_post(const char* endpoint, const char* json_ // Protected by g_device_jni_state.mtx for thread safety static std::string g_cached_device_id; -// Helper to extract a string value from JSON (simple parser for known keys) -// Returns allocated string that must be stored persistently, or nullptr -static std::string extract_json_string(const char* json, const char* key) { - if (!json || !key) - return ""; - - std::string search_key = "\"" + std::string(key) + "\":"; - const char* pos = strstr(json, search_key.c_str()); - if (!pos) - return ""; - - pos += search_key.length(); - while (*pos == ' ') - pos++; - - if (*pos == 'n' && strncmp(pos, "null", 4) == 0) { - return ""; - } - - if (*pos != '"') - return ""; - pos++; - - const char* end = strchr(pos, '"'); - if (!end) - return ""; - - return std::string(pos, end - pos); -} - -// Helper to extract an integer value from JSON -static int64_t extract_json_int(const char* json, const char* key) { - if (!json || !key) - return 0; - - std::string search_key = "\"" + std::string(key) + "\":"; - const char* pos = strstr(json, search_key.c_str()); - if (!pos) - return 0; - - pos += search_key.length(); - while (*pos == ' ') - pos++; - - return strtoll(pos, nullptr, 10); -} - -// Helper to extract a boolean value from JSON -static bool extract_json_bool(const char* json, const char* key) { - if (!json || !key) - return false; - - std::string search_key = "\"" + std::string(key) + "\":"; - const char* pos = strstr(json, search_key.c_str()); - if (!pos) - return false; - - pos += search_key.length(); - while (*pos == ' ') - pos++; - - return strncmp(pos, "true", 4) == 0; -} - // Static storage for device info strings (need to persist for C callbacks) static struct { std::string device_id; @@ -2370,25 +2265,46 @@ static void jni_device_get_info(rac_device_registration_info_t* out_info, void* } if (jResult && out_info) { - const char* json = env->GetStringUTFChars(jResult, nullptr); - LOGd("jni_device_get_info: parsing JSON: %.200s...", json); + const char* json_str = env->GetStringUTFChars(jResult, nullptr); + LOGd("jni_device_get_info: parsing JSON: %.200s...", json_str); // Parse JSON and extract all fields std::lock_guard lock(g_device_info_strings.mtx); - // Extract all string fields from Kotlin's getDeviceInfoCallback() JSON - g_device_info_strings.device_id = extract_json_string(json, "device_id"); - g_device_info_strings.device_model = extract_json_string(json, "device_model"); - g_device_info_strings.device_name = extract_json_string(json, "device_name"); - g_device_info_strings.platform = extract_json_string(json, "platform"); - g_device_info_strings.os_version = extract_json_string(json, "os_version"); - g_device_info_strings.form_factor = extract_json_string(json, "form_factor"); - g_device_info_strings.architecture = extract_json_string(json, "architecture"); - g_device_info_strings.chip_name = extract_json_string(json, "chip_name"); - g_device_info_strings.gpu_family = extract_json_string(json, "gpu_family"); - g_device_info_strings.battery_state = extract_json_string(json, "battery_state"); - g_device_info_strings.device_fingerprint = extract_json_string(json, "device_fingerprint"); - g_device_info_strings.manufacturer = extract_json_string(json, "manufacturer"); + try { + auto j = nlohmann::json::parse(json_str); + + // Extract all string fields from Kotlin's getDeviceInfoCallback() JSON + g_device_info_strings.device_id = j.value("device_id", std::string("")); + g_device_info_strings.device_model = j.value("device_model", std::string("")); + g_device_info_strings.device_name = j.value("device_name", std::string("")); + g_device_info_strings.platform = j.value("platform", std::string("")); + g_device_info_strings.os_version = j.value("os_version", std::string("")); + g_device_info_strings.form_factor = j.value("form_factor", std::string("")); + g_device_info_strings.architecture = j.value("architecture", std::string("")); + g_device_info_strings.chip_name = j.value("chip_name", std::string("")); + g_device_info_strings.gpu_family = j.value("gpu_family", std::string("")); + g_device_info_strings.battery_state = j.value("battery_state", std::string("")); + g_device_info_strings.device_fingerprint = j.value("device_fingerprint", std::string("")); + g_device_info_strings.manufacturer = j.value("manufacturer", std::string("")); + + // Extract integer fields + out_info->total_memory = j.value("total_memory", (int64_t)0); + out_info->available_memory = j.value("available_memory", (int64_t)0); + out_info->neural_engine_cores = j.value("neural_engine_cores", (int32_t)0); + out_info->core_count = j.value("core_count", (int32_t)0); + out_info->performance_cores = j.value("performance_cores", (int32_t)0); + out_info->efficiency_cores = j.value("efficiency_cores", (int32_t)0); + + // Extract boolean fields + out_info->has_neural_engine = j.value("has_neural_engine", false) ? RAC_TRUE : RAC_FALSE; + out_info->is_low_power_mode = j.value("is_low_power_mode", false) ? RAC_TRUE : RAC_FALSE; + + // Extract float field for battery + out_info->battery_level = j.value("battery_level", 0.0f); + } catch (const nlohmann::json::exception& e) { + LOGe("Failed to parse device info JSON: %s", e.what()); + } // Assign pointers to out_info (C struct uses const char*) out_info->device_id = g_device_info_strings.device_id.empty() @@ -2425,32 +2341,12 @@ static void jni_device_get_info(rac_device_registration_info_t* out_info, void* ? nullptr : g_device_info_strings.device_fingerprint.c_str(); - // Extract integer fields - out_info->total_memory = extract_json_int(json, "total_memory"); - out_info->available_memory = extract_json_int(json, "available_memory"); - out_info->neural_engine_cores = - static_cast(extract_json_int(json, "neural_engine_cores")); - out_info->core_count = static_cast(extract_json_int(json, "core_count")); - out_info->performance_cores = - static_cast(extract_json_int(json, "performance_cores")); - out_info->efficiency_cores = - static_cast(extract_json_int(json, "efficiency_cores")); - - // Extract boolean fields - out_info->has_neural_engine = - extract_json_bool(json, "has_neural_engine") ? RAC_TRUE : RAC_FALSE; - out_info->is_low_power_mode = - extract_json_bool(json, "is_low_power_mode") ? RAC_TRUE : RAC_FALSE; - - // Extract float field for battery - out_info->battery_level = static_cast(extract_json_int(json, "battery_level")); - LOGi("jni_device_get_info: parsed device_model=%s, os_version=%s, architecture=%s", out_info->device_model ? out_info->device_model : "(null)", out_info->os_version ? out_info->os_version : "(null)", out_info->architecture ? out_info->architecture : "(null)"); - env->ReleaseStringUTFChars(jResult, json); + env->ReleaseStringUTFChars(jResult, json_str); env->DeleteLocalRef(jResult); } } diff --git a/sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_llm.dart b/sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_llm.dart index de303e452..aff43aa66 100644 --- a/sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_llm.dart +++ b/sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_llm.dart @@ -229,6 +229,7 @@ class DartBridgeLLM { /// [prompt] - Input prompt. /// [maxTokens] - Maximum tokens to generate (default: 512). /// [temperature] - Sampling temperature (default: 0.7). + /// [systemPrompt] - Optional system prompt for model behavior (default: null). /// /// Returns the generated text and metrics. /// @@ -238,6 +239,7 @@ class DartBridgeLLM { String prompt, { int maxTokens = 512, double temperature = 0.7, + String? systemPrompt, }) async { final handle = getHandle(); @@ -251,8 +253,10 @@ class DartBridgeLLM { final tokens = maxTokens; final temp = temperature; + _logger.debug('[PARAMS] generate: temperature=$temperature, maxTokens=$maxTokens, systemPrompt=${systemPrompt != null ? "set(${systemPrompt.length} chars)" : "nil"}'); + final result = await Isolate.run(() { - return _generateInIsolate(handleAddress, prompt, tokens, temp); + return _generateInIsolate(handleAddress, prompt, tokens, temp, systemPrompt); }); if (result.error != null) { @@ -270,7 +274,7 @@ class DartBridgeLLM { /// Generate text with streaming. /// /// Returns a stream of tokens as they are generated. - /// + /// /// ARCHITECTURE: Runs in a background isolate to prevent ANR. /// The logger callback uses NativeCallable.listener which is thread-safe. /// Tokens are sent back to the main isolate via SendPort for UI updates. @@ -278,6 +282,7 @@ class DartBridgeLLM { String prompt, { int maxTokens = 512, // Can use higher values now since it's non-blocking double temperature = 0.7, + String? systemPrompt, }) { final handle = getHandle(); @@ -288,12 +293,15 @@ class DartBridgeLLM { // Create stream controller for emitting tokens to the caller final controller = StreamController(); + _logger.debug('[PARAMS] generateStream: temperature=$temperature, maxTokens=$maxTokens, systemPrompt=${systemPrompt != null ? "set(${systemPrompt.length} chars)" : "nil"}'); + // Start streaming generation in a background isolate unawaited(_startBackgroundStreaming( handle.address, prompt, maxTokens, temperature, + systemPrompt, controller, )); @@ -301,7 +309,7 @@ class DartBridgeLLM { } /// Start streaming generation in a background isolate. - /// + /// /// ARCHITECTURE NOTE: /// The logger callback now uses NativeCallable.listener which is thread-safe. /// This allows us to run the FFI streaming call in a background isolate @@ -312,6 +320,7 @@ class DartBridgeLLM { String prompt, int maxTokens, double temperature, + String? systemPrompt, StreamController controller, ) async { // Create a ReceivePort to receive tokens from the background isolate @@ -346,6 +355,7 @@ class DartBridgeLLM { prompt: prompt, maxTokens: maxTokens, temperature: temperature, + systemPrompt: systemPrompt, ), ); } catch (e) { @@ -430,6 +440,7 @@ class _StreamingIsolateParams { final String prompt; final int maxTokens; final double temperature; + final String? systemPrompt; _StreamingIsolateParams({ required this.sendPort, @@ -437,6 +448,7 @@ class _StreamingIsolateParams { required this.prompt, required this.maxTokens, required this.temperature, + this.systemPrompt, }); } @@ -460,6 +472,7 @@ void _streamingIsolateEntry(_StreamingIsolateParams params) { final handle = Pointer.fromAddress(params.handleAddress); final promptPtr = params.prompt.toNativeUtf8(); final optionsPtr = calloc(); + Pointer? systemPromptPtr; try { // Set options @@ -469,7 +482,14 @@ void _streamingIsolateEntry(_StreamingIsolateParams params) { optionsPtr.ref.stopSequences = nullptr; optionsPtr.ref.numStopSequences = 0; optionsPtr.ref.streamingEnabled = RAC_TRUE; - optionsPtr.ref.systemPrompt = nullptr; + + // Set systemPrompt if provided + if (params.systemPrompt != null && params.systemPrompt!.isNotEmpty) { + systemPromptPtr = params.systemPrompt!.toNativeUtf8(); + optionsPtr.ref.systemPrompt = systemPromptPtr!; + } else { + optionsPtr.ref.systemPrompt = nullptr; + } final lib = PlatformLoader.loadCommons(); @@ -532,6 +552,9 @@ void _streamingIsolateEntry(_StreamingIsolateParams params) { } finally { calloc.free(promptPtr); calloc.free(optionsPtr); + if (systemPromptPtr != null) { + calloc.free(systemPromptPtr!); + } _isolateSendPort = null; } } @@ -579,11 +602,13 @@ _IsolateGenerationResult _generateInIsolate( String prompt, int maxTokens, double temperature, + String? systemPrompt, ) { final handle = Pointer.fromAddress(handleAddress); final promptPtr = prompt.toNativeUtf8(); final optionsPtr = calloc(); final resultPtr = calloc(); + Pointer? systemPromptPtr; try { // Set options - matching C++ rac_llm_options_t @@ -593,7 +618,14 @@ _IsolateGenerationResult _generateInIsolate( optionsPtr.ref.stopSequences = nullptr; optionsPtr.ref.numStopSequences = 0; optionsPtr.ref.streamingEnabled = RAC_FALSE; - optionsPtr.ref.systemPrompt = nullptr; + + // Set systemPrompt if provided + if (systemPrompt != null && systemPrompt.isNotEmpty) { + systemPromptPtr = systemPrompt.toNativeUtf8(); + optionsPtr.ref.systemPrompt = systemPromptPtr!; + } else { + optionsPtr.ref.systemPrompt = nullptr; + } final lib = PlatformLoader.loadCommons(); final generateFn = lib.lookupFunction< @@ -625,5 +657,8 @@ _IsolateGenerationResult _generateInIsolate( calloc.free(promptPtr); calloc.free(optionsPtr); calloc.free(resultPtr); + if (systemPromptPtr != null) { + calloc.free(systemPromptPtr!); + } } } diff --git a/sdk/runanywhere-flutter/packages/runanywhere/lib/public/runanywhere.dart b/sdk/runanywhere-flutter/packages/runanywhere/lib/public/runanywhere.dart index 7096e22ec..ab6d24745 100644 --- a/sdk/runanywhere-flutter/packages/runanywhere/lib/public/runanywhere.dart +++ b/sdk/runanywhere-flutter/packages/runanywhere/lib/public/runanywhere.dart @@ -1192,6 +1192,7 @@ class RunAnywhere { prompt, maxTokens: opts.maxTokens, temperature: opts.temperature, + systemPrompt: opts.systemPrompt, ); final endTime = DateTime.now(); @@ -1292,6 +1293,7 @@ class RunAnywhere { prompt, maxTokens: opts.maxTokens, temperature: opts.temperature, + systemPrompt: opts.systemPrompt, ); // Forward tokens and collect them, track subscription in bridge for cancellation diff --git a/sdk/runanywhere-kotlin/src/commonMain/kotlin/com/runanywhere/sdk/public/extensions/LLM/LLMTypes.kt b/sdk/runanywhere-kotlin/src/commonMain/kotlin/com/runanywhere/sdk/public/extensions/LLM/LLMTypes.kt index 5e8b549d1..5a28f0d03 100644 --- a/sdk/runanywhere-kotlin/src/commonMain/kotlin/com/runanywhere/sdk/public/extensions/LLM/LLMTypes.kt +++ b/sdk/runanywhere-kotlin/src/commonMain/kotlin/com/runanywhere/sdk/public/extensions/LLM/LLMTypes.kt @@ -104,8 +104,8 @@ data class LLMConfiguration( */ @Serializable data class LLMGenerationOptions( - val maxTokens: Int = 100, - val temperature: Float = 0.8f, + val maxTokens: Int = 1000, + val temperature: Float = 0.7f, val topP: Float = 1.0f, val stopSequences: List = emptyList(), val streamingEnabled: Boolean = false, diff --git a/sdk/runanywhere-kotlin/src/jvmAndroidMain/kotlin/com/runanywhere/sdk/foundation/bridge/extensions/CppBridgeLLM.kt b/sdk/runanywhere-kotlin/src/jvmAndroidMain/kotlin/com/runanywhere/sdk/foundation/bridge/extensions/CppBridgeLLM.kt index ab1c6321c..2a64304d3 100644 --- a/sdk/runanywhere-kotlin/src/jvmAndroidMain/kotlin/com/runanywhere/sdk/foundation/bridge/extensions/CppBridgeLLM.kt +++ b/sdk/runanywhere-kotlin/src/jvmAndroidMain/kotlin/com/runanywhere/sdk/foundation/bridge/extensions/CppBridgeLLM.kt @@ -220,6 +220,7 @@ object CppBridgeLLM { * @param repeatPenalty Penalty for repeating tokens * @param stopSequences List of sequences that stop generation * @param seed Random seed for reproducibility (-1 for random) + * @param systemPrompt System prompt for LLM (optional) */ data class GenerationConfig( val maxTokens: Int = 512, @@ -229,6 +230,7 @@ object CppBridgeLLM { val repeatPenalty: Float = 1.1f, val stopSequences: List = emptyList(), val seed: Long = -1, + val systemPrompt: String? = null, ) { /** * Convert to JSON string for C++ interop. @@ -247,7 +249,13 @@ object CppBridgeLLM { append("\"${escapeJson(seq)}\"") } append("],") - append("\"seed\":$seed") + append("\"seed\":$seed,") + append("\"system_prompt\":") + if (systemPrompt != null) { + append("\"${escapeJson(systemPrompt)}\"") + } else { + append("null") + } append("}") } } diff --git a/sdk/runanywhere-kotlin/src/jvmAndroidMain/kotlin/com/runanywhere/sdk/public/extensions/RunAnywhere+TextGeneration.jvmAndroid.kt b/sdk/runanywhere-kotlin/src/jvmAndroidMain/kotlin/com/runanywhere/sdk/public/extensions/RunAnywhere+TextGeneration.jvmAndroid.kt index 1eb6d6545..c86ab3a0c 100644 --- a/sdk/runanywhere-kotlin/src/jvmAndroidMain/kotlin/com/runanywhere/sdk/public/extensions/RunAnywhere+TextGeneration.jvmAndroid.kt +++ b/sdk/runanywhere-kotlin/src/jvmAndroidMain/kotlin/com/runanywhere/sdk/public/extensions/RunAnywhere+TextGeneration.jvmAndroid.kt @@ -50,8 +50,11 @@ actual suspend fun RunAnywhere.generate( maxTokens = opts.maxTokens, temperature = opts.temperature, topP = opts.topP, + systemPrompt = opts.systemPrompt, ) + llmLogger.info("[PARAMS] generate: temperature=${opts.temperature}, top_p=${opts.topP}, max_tokens=${opts.maxTokens}, system_prompt=${opts.systemPrompt?.let { "set(${it.length} chars)" } ?: "nil"}, streaming=false") + // Call CppBridgeLLM to generate val cppResult = CppBridgeLLM.generate(prompt, config) @@ -85,11 +88,14 @@ actual fun RunAnywhere.generateStream( val opts = options ?: LLMGenerationOptions.DEFAULT + llmLogger.info("[PARAMS] generateStream: temperature=${opts.temperature}, top_p=${opts.topP}, max_tokens=${opts.maxTokens}, system_prompt=${opts.systemPrompt?.let { "set(${it.length} chars)" } ?: "nil"}, streaming=true") + val config = CppBridgeLLM.GenerationConfig( maxTokens = opts.maxTokens, temperature = opts.temperature, topP = opts.topP, + systemPrompt = opts.systemPrompt, ) // Use a channel to bridge callback to flow @@ -132,11 +138,14 @@ actual suspend fun RunAnywhere.generateStreamWithMetrics( var tokenCount = 0 var firstTokenTime: Long? = null + llmLogger.info("[PARAMS] generateStreamWithMetrics: temperature=${opts.temperature}, top_p=${opts.topP}, max_tokens=${opts.maxTokens}, system_prompt=${opts.systemPrompt?.let { "set(${it.length} chars)" } ?: "nil"}, streaming=true") + val config = CppBridgeLLM.GenerationConfig( maxTokens = opts.maxTokens, temperature = opts.temperature, topP = opts.topP, + systemPrompt = opts.systemPrompt, ) // Use a channel to bridge callback to flow diff --git a/sdk/runanywhere-react-native/packages/core/cpp/HybridRunAnywhereCore.cpp b/sdk/runanywhere-react-native/packages/core/cpp/HybridRunAnywhereCore.cpp index 11638f87e..c4577355c 100644 --- a/sdk/runanywhere-react-native/packages/core/cpp/HybridRunAnywhereCore.cpp +++ b/sdk/runanywhere-react-native/packages/core/cpp/HybridRunAnywhereCore.cpp @@ -1416,15 +1416,18 @@ std::shared_ptr> HybridRunAnywhereCore::generate( // Parse options int maxTokens = 256; float temperature = 0.7f; + std::string systemPrompt; if (optionsJson.has_value()) { maxTokens = extractIntValue(optionsJson.value(), "max_tokens", 256); temperature = static_cast(extractDoubleValue(optionsJson.value(), "temperature", 0.7)); + systemPrompt = extractStringValue(optionsJson.value(), "system_prompt", ""); } rac_llm_options_t options = {}; options.max_tokens = maxTokens; options.temperature = temperature; options.top_p = 0.9f; + options.system_prompt = systemPrompt.empty() ? nullptr : systemPrompt.c_str(); rac_llm_result_t llmResult = {}; rac_result_t result = rac_llm_component_generate(handle, prompt.c_str(), &options, &llmResult); @@ -1514,10 +1517,13 @@ std::shared_ptr> HybridRunAnywhereCore::generateStream( } // Parse options + std::string systemPrompt = extractStringValue(optionsJson, "system_prompt", ""); + rac_llm_options_t options = {}; options.max_tokens = extractIntValue(optionsJson, "max_tokens", 256); options.temperature = static_cast(extractDoubleValue(optionsJson, "temperature", 0.7)); options.top_p = 0.9f; + options.system_prompt = systemPrompt.empty() ? nullptr : systemPrompt.c_str(); // Create streaming context LLMStreamContext ctx; @@ -1590,14 +1596,17 @@ std::shared_ptr> HybridRunAnywhereCore::generateStructured( } // Generate with the prepared prompt + std::string systemPrompt; rac_llm_options_t options = {}; if (optionsJson.has_value()) { options.max_tokens = extractIntValue(optionsJson.value(), "max_tokens", 512); options.temperature = static_cast(extractDoubleValue(optionsJson.value(), "temperature", 0.7)); + systemPrompt = extractStringValue(optionsJson.value(), "system_prompt", ""); } else { options.max_tokens = 512; options.temperature = 0.7f; } + options.system_prompt = systemPrompt.empty() ? nullptr : systemPrompt.c_str(); rac_llm_result_t llmResult = {}; rac_result_t result = rac_llm_component_generate(handle, preparedPrompt, &options, &llmResult); diff --git a/sdk/runanywhere-swift/Sources/RunAnywhere/Public/Extensions/LLM/RunAnywhere+StructuredOutput.swift b/sdk/runanywhere-swift/Sources/RunAnywhere/Public/Extensions/LLM/RunAnywhere+StructuredOutput.swift index 2656ae702..4f3a27d9f 100644 --- a/sdk/runanywhere-swift/Sources/RunAnywhere/Public/Extensions/LLM/RunAnywhere+StructuredOutput.swift +++ b/sdk/runanywhere-swift/Sources/RunAnywhere/Public/Extensions/LLM/RunAnywhere+StructuredOutput.swift @@ -249,10 +249,21 @@ public extension RunAnywhere { cOptions.top_p = options.topP cOptions.streaming_enabled = RAC_FALSE - // Generate + // Generate - wrap in system_prompt lifetime scope var llmResult = rac_llm_result_t() - let generateResult = prompt.withCString { promptPtr in - rac_llm_component_generate(handle, promptPtr, &cOptions, &llmResult) + let generateResult: rac_result_t + if let systemPrompt = options.systemPrompt { + generateResult = systemPrompt.withCString { sysPromptPtr in + cOptions.system_prompt = sysPromptPtr + return prompt.withCString { promptPtr in + rac_llm_component_generate(handle, promptPtr, &cOptions, &llmResult) + } + } + } else { + cOptions.system_prompt = nil + generateResult = prompt.withCString { promptPtr in + rac_llm_component_generate(handle, promptPtr, &cOptions, &llmResult) + } } guard generateResult == RAC_SUCCESS else { diff --git a/sdk/runanywhere-swift/Sources/RunAnywhere/Public/Extensions/LLM/RunAnywhere+TextGeneration.swift b/sdk/runanywhere-swift/Sources/RunAnywhere/Public/Extensions/LLM/RunAnywhere+TextGeneration.swift index 5353259ee..780b81a17 100644 --- a/sdk/runanywhere-swift/Sources/RunAnywhere/Public/Extensions/LLM/RunAnywhere+TextGeneration.swift +++ b/sdk/runanywhere-swift/Sources/RunAnywhere/Public/Extensions/LLM/RunAnywhere+TextGeneration.swift @@ -58,10 +58,23 @@ public extension RunAnywhere { cOptions.top_p = opts.topP cOptions.streaming_enabled = RAC_FALSE - // Generate (C++ emits events) + SDKLogger.llm.info("[PARAMS] generate: temperature=\(cOptions.temperature), top_p=\(cOptions.top_p), max_tokens=\(cOptions.max_tokens), system_prompt=\(opts.systemPrompt != nil ? "set(\(opts.systemPrompt!.count) chars)" : "nil"), streaming=\(cOptions.streaming_enabled == RAC_TRUE)") + + // Generate (C++ emits events) - wrap in system_prompt lifetime scope var llmResult = rac_llm_result_t() - let generateResult = prompt.withCString { promptPtr in - rac_llm_component_generate(handle, promptPtr, &cOptions, &llmResult) + let generateResult: rac_result_t + if let systemPrompt = opts.systemPrompt { + generateResult = systemPrompt.withCString { sysPromptPtr in + cOptions.system_prompt = sysPromptPtr + return prompt.withCString { promptPtr in + rac_llm_component_generate(handle, promptPtr, &cOptions, &llmResult) + } + } + } else { + cOptions.system_prompt = nil + generateResult = prompt.withCString { promptPtr in + rac_llm_component_generate(handle, promptPtr, &cOptions, &llmResult) + } } guard generateResult == RAC_SUCCESS else { @@ -148,11 +161,14 @@ public extension RunAnywhere { cOptions.top_p = opts.topP cOptions.streaming_enabled = RAC_TRUE + SDKLogger.llm.info("[PARAMS] generateStream: temperature=\(cOptions.temperature), top_p=\(cOptions.top_p), max_tokens=\(cOptions.max_tokens), system_prompt=\(opts.systemPrompt != nil ? "set(\(opts.systemPrompt!.count) chars)" : "nil"), streaming=\(cOptions.streaming_enabled == RAC_TRUE)") + let stream = createTokenStream( prompt: prompt, handle: handle, options: cOptions, - collector: collector + collector: collector, + systemPrompt: opts.systemPrompt ) let resultTask = Task { @@ -168,7 +184,8 @@ public extension RunAnywhere { prompt: String, handle: UnsafeMutableRawPointer, options: rac_llm_options_t, - collector: LLMStreamingMetricsCollector + collector: LLMStreamingMetricsCollector, + systemPrompt: String? = nil ) -> AsyncThrowingStream { AsyncThrowingStream { continuation in Task { @@ -181,16 +198,29 @@ public extension RunAnywhere { let callbacks = LLMStreamCallbacks.create() var cOptions = options - let streamResult = prompt.withCString { promptPtr in - rac_llm_component_generate_stream( - handle, - promptPtr, - &cOptions, - callbacks.token, - callbacks.complete, - callbacks.error, - contextPtr - ) + let callCFunction: () -> rac_result_t = { + prompt.withCString { promptPtr in + rac_llm_component_generate_stream( + handle, + promptPtr, + &cOptions, + callbacks.token, + callbacks.complete, + callbacks.error, + contextPtr + ) + } + } + + let streamResult: rac_result_t + if let systemPrompt = systemPrompt { + streamResult = systemPrompt.withCString { sysPtr in + cOptions.system_prompt = sysPtr + return callCFunction() + } + } else { + cOptions.system_prompt = nil + streamResult = callCFunction() } if streamResult != RAC_SUCCESS {