Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
68 commits
Select commit Hold shift + click to select a range
409c108
feat: Add tool calling support with C++ as single source of truth
jmakwana06 Jan 25, 2026
e1b806e
fix: Address code review feedback
jmakwana06 Jan 25, 2026
ea1ed92
fix(ios): not deleting models once downloaded
Hyunoh-Yeo Jan 25, 2026
5e1244b
fix(ios): not deleting models once downloaded
Hyunoh-Yeo Jan 25, 2026
e827a4d
fix(ios): not deleting models once downloaded
Hyunoh-Yeo Jan 25, 2026
bf1fd5e
fix(ios): wrong metadata displaying
Hyunoh-Yeo Jan 25, 2026
a766075
fix(ios): wrong metadata displaying
Hyunoh-Yeo Jan 25, 2026
cc577cf
fixes
shubhammalhotra28 Jan 25, 2026
1f1b42a
adding tab - to just see what's happenin when doing tool call - will …
shubhammalhotra28 Jan 25, 2026
cd26d3a
fix(ios): disable trash for Platform LLM/TTS
Hyunoh-Yeo Jan 25, 2026
1ec933e
Merge pull request #293 from j-makwana/feature/tool-calling
sanchitmonga22 Jan 26, 2026
f58f2db
git ignore updates
shubhammalhotra28 Jan 26, 2026
21c8ca8
git ignore updates
shubhammalhotra28 Jan 26, 2026
6703de9
Merge pull request #300 from RunanywhereAI/feature/tool-calling
shubhammalhotra28 Jan 26, 2026
9311ff8
Merge pull request #298 from Hyunoh-Yeo/bugfix/272
shubhammalhotra28 Jan 26, 2026
d0022cb
feat(swift): Add strongly-typed tool calling support to Swift SDK
sanchitmonga22 Jan 26, 2026
57315b1
feat(swift): Enhance tool calling with real API integration and impro…
sanchitmonga22 Jan 26, 2026
cf43e7b
small change
sanchitmonga22 Jan 26, 2026
018e07f
initial changes
shubhammalhotra28 Jan 27, 2026
5b1f78c
updates
shubhammalhotra28 Jan 28, 2026
5281ac0
fixes for swift
shubhammalhotra28 Jan 28, 2026
93c9a8f
minor fixes
shubhammalhotra28 Jan 28, 2026
e92681e
kotlin and swift works
shubhammalhotra28 Jan 28, 2026
f0e04bc
fixing flutter sdk
shubhammalhotra28 Jan 29, 2026
f98320c
fix flutter
shubhammalhotra28 Jan 29, 2026
9d37274
flutter changes
shubhammalhotra28 Jan 29, 2026
9d2605d
stagng flutter changes
shubhammalhotra28 Jan 29, 2026
ed52fcd
flutter changes
shubhammalhotra28 Jan 30, 2026
09644a1
tweaks fo rflutter
shubhammalhotra28 Jan 30, 2026
e29e042
react-native nweaks
shubhammalhotra28 Jan 30, 2026
2c5d44a
fixing react native
shubhammalhotra28 Jan 30, 2026
88a1239
react-native looks good
shubhammalhotra28 Jan 30, 2026
30596a7
fix: Address PR #309 review comments - Quick fixes and critical issues
sanchitmonga22 Jan 30, 2026
a6b8eea
fix: Address PR #309 larger structural issues
sanchitmonga22 Jan 30, 2026
0c18ecb
fix: Eliminate React Native prompt formatting duplication
sanchitmonga22 Jan 31, 2026
12599fc
small fixes
shubhammalhotra28 Jan 30, 2026
0b4e8eb
Merge main into shubham/fix-tool-calling
shubhammalhotra28 Feb 1, 2026
fb4663f
minor fixes
shubhammalhotra28 Feb 2, 2026
fb8e9dd
Merge pull request #309 from RunanywhereAI/shubham/fix-tool-calling
shubhammalhotra28 Feb 2, 2026
f84e929
ci(swift): add auto-tag workflow on push to main
josuediazflores Feb 2, 2026
e728114
Merge pull request #1 from josuediazflores/ci/swift-auto-tag
josuediazflores Feb 2, 2026
30c23a6
Phase 2 Complete
josuediazflores Feb 2, 2026
00c1eba
Fix
josuediazflores Feb 2, 2026
cb07423
Fix Again
josuediazflores Feb 2, 2026
9dfb4ec
ci(swift): create semver tag in Phase 2 for SPM resolution
josuediazflores Feb 2, 2026
37ad693
validation: point swift-spm-consumer at fork for Phase 2 test
josuediazflores Feb 2, 2026
cfe813b
chore(swift): use remote binaries on main for consumer (v0.17.5)
josuediazflores Feb 2, 2026
fcbe222
SDKTestApp, TTS/LLM UI, upstream URLs, SDK improvements
josuediazflores Feb 2, 2026
1328ad3
ci(swift): Swift CI/CD only – auto-tag, build-release, release, Packa…
josuediazflores Feb 2, 2026
7f21179
ci(swift): harden tag build release workflows
josuediazflores Feb 2, 2026
98336e9
ci(swift): ensure commons built before swift package build
josuediazflores Feb 2, 2026
9127453
ci(swift): pin Xcode 15.4 for swift-v tag build
josuediazflores Feb 3, 2026
459abcf
feat(ios): add SDKTestApp demo app
josuediazflores Feb 3, 2026
2139b85
fix(swift): resolve strict concurrency build errors
josuediazflores Feb 3, 2026
6d83bcb
Merge branch 'main' into ci/swift-cd-only
josuediazflores Feb 3, 2026
892c82a
fix(swift): Swift 6 strict concurrency – temp storage, Sendable ref
josuediazflores Feb 3, 2026
0c5f063
fix(swift): use resultPtr.pointee in Foundation Models generate catch
josuediazflores Feb 3, 2026
b1fa241
fix(ci): correct zip path for release-assets (3 levels up from common…
josuediazflores Feb 3, 2026
3f4934d
fix(ci): checkout main before pushing Package.swift, merge tag steps
josuediazflores Feb 3, 2026
93ddf5e
chore: trigger PR refresh
josuediazflores Feb 3, 2026
129a799
fix(ci): build commons once; Swift SDK step only consumes 3 XCFrameworks
josuediazflores Feb 3, 2026
c884700
fix(ci): discard local Package.swift changes before checkout main
josuediazflores Feb 3, 2026
124fa8b
fix(ci): guard tag/main alignment; use git pull --ff-only to prevent …
josuediazflores Feb 3, 2026
7055a3d
fix(codeql): C/C++ manual build for runanywhere-commons; disable defa…
josuediazflores Feb 4, 2026
7c38b94
chore: trigger CodeQL run with latest workflow
josuediazflores Feb 4, 2026
0d4c26b
fix(ci): run tag/main guard only on upstream; allow release on forks …
josuediazflores Feb 4, 2026
af0ae2a
Merge PR 323: ci/swift-cd-only
shubhammalhotra28 Feb 8, 2026
e77ac2c
cleanup
shubhammalhotra28 Feb 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,21 @@ class RunAnywhereApplication : Application() {
framework = InferenceFramework.LLAMA_CPP,
memoryRequirement = 400_000_000,
)
// LFM2-Tool models - For tool calling / function calling support
RunAnywhere.registerModel(
id = "lfm2-1.2b-tool-q4_k_m",
name = "LiquidAI LFM2 1.2B Tool Q4_K_M",
url = "https://huggingface.co/LiquidAI/LFM2-1.2B-Tool-GGUF/resolve/main/LFM2-1.2B-Tool-Q4_K_M.gguf",
framework = InferenceFramework.LLAMA_CPP,
memoryRequirement = 800_000_000,
)
RunAnywhere.registerModel(
id = "lfm2-1.2b-tool-q8_0",
name = "LiquidAI LFM2 1.2B Tool Q8_0",
url = "https://huggingface.co/LiquidAI/LFM2-1.2B-Tool-GGUF/resolve/main/LFM2-1.2B-Tool-Q8_0.gguf",
framework = InferenceFramework.LLAMA_CPP,
memoryRequirement = 1_400_000_000,
)
Log.i("RunAnywhereApp", "✅ LLM models registered")

// Register ONNX STT and TTS models
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,19 @@ data class PerformanceSummary(
val successRate: Double = 1.0,
)

/**
* App-local tool call info.
* Matches iOS ToolCallInfo exactly.
*/
@Serializable
data class ToolCallInfo(
val toolName: String,
val arguments: String, // JSON string for display
val result: String? = null, // JSON string for display
val success: Boolean,
val error: String? = null,
)

/**
* App-specific ChatMessage for conversations.
* Self-contained with app-local types.
Expand All @@ -130,6 +143,7 @@ data class ChatMessage(
val timestamp: Long = System.currentTimeMillis(),
val analytics: MessageAnalytics? = null,
val modelInfo: MessageModelInfo? = null,
val toolCallInfo: ToolCallInfo? = null,
val metadata: Map<String, String>? = null,
) {
val isFromUser: Boolean get() = role == MessageRole.USER
Expand Down Expand Up @@ -158,6 +172,7 @@ data class ChatMessage(
thinkingContent: String? = null,
analytics: MessageAnalytics? = null,
modelInfo: MessageModelInfo? = null,
toolCallInfo: ToolCallInfo? = null,
metadata: Map<String, String>? = null,
): ChatMessage =
ChatMessage(
Expand All @@ -166,6 +181,7 @@ data class ChatMessage(
thinkingContent = thinkingContent,
analytics = analytics,
modelInfo = modelInfo,
toolCallInfo = toolCallInfo,
metadata = metadata,
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,9 @@ import com.runanywhere.runanywhereai.data.ConversationStore
import com.runanywhere.runanywhereai.domain.models.ChatMessage
import com.runanywhere.runanywhereai.domain.models.Conversation
import com.runanywhere.runanywhereai.domain.models.MessageRole
import com.runanywhere.runanywhereai.presentation.settings.ToolSettingsViewModel
import com.runanywhere.runanywhereai.ui.theme.AppColors
import android.app.Application
import com.runanywhere.runanywhereai.ui.theme.AppTypography
import com.runanywhere.runanywhereai.ui.theme.Dimensions
import kotlinx.coroutines.launch
Expand Down Expand Up @@ -238,6 +240,20 @@ fun ChatScreen(viewModel: ChatViewModel = viewModel()) {
color = MaterialTheme.colorScheme.outline,
)

// Tool calling indicator - matching iOS
val toolContext = LocalContext.current
val application = toolContext.applicationContext as Application
val toolSettingsViewModel = remember { ToolSettingsViewModel.getInstance(application) }
val toolState by toolSettingsViewModel.uiState.collectAsStateWithLifecycle()

AnimatedVisibility(
visible = toolState.toolCallingEnabled && toolState.registeredTools.isNotEmpty(),
enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically(),
) {
ToolCallingBadge(toolCount = toolState.registeredTools.size)
}

// Model selection prompt (when no model loaded) - matching iOS
AnimatedVisibility(
visible = !uiState.isModelLoaded,
Expand Down Expand Up @@ -457,6 +473,8 @@ fun MessageBubbleView(
message: ChatMessage,
isGenerating: Boolean = false,
) {
var showToolCallSheet by remember { mutableStateOf(false) }

val alignment =
if (message.role == MessageRole.USER) {
Arrangement.End
Expand Down Expand Up @@ -498,6 +516,15 @@ fun MessageBubbleView(
Spacer(modifier = Modifier.height(Dimensions.small))
}

// Tool call indicator (for assistant messages with tool calls) - matching iOS
if (message.role == MessageRole.ASSISTANT && message.toolCallInfo != null) {
com.runanywhere.runanywhereai.presentation.chat.components.ToolCallIndicator(
toolCallInfo = message.toolCallInfo,
onTap = { showToolCallSheet = true },
)
Spacer(modifier = Modifier.height(Dimensions.small))
}

// Thinking toggle (if thinking content exists) - matching iOS
message.thinkingContent?.let { thinking ->
ThinkingToggle(
Expand Down Expand Up @@ -650,6 +677,14 @@ fun MessageBubbleView(
Spacer(modifier = Modifier.width(Dimensions.messageBubbleMinSpacing))
}
}

// Tool call detail sheet - matching iOS
if (showToolCallSheet && message.toolCallInfo != null) {
com.runanywhere.runanywhereai.presentation.chat.components.ToolCallDetailSheet(
toolCallInfo = message.toolCallInfo,
onDismiss = { showToolCallSheet = false },
)
}
}

// Helper function to format timestamp - matching iOS
Expand Down Expand Up @@ -1118,6 +1153,42 @@ fun EmptyStateView(
// MODEL SELECTION PROMPT
// ====================

/**
* Tool calling indicator badge - matching iOS ChatInterfaceView toolCallingBadge
*/
@Composable
fun ToolCallingBadge(toolCount: Int) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = Dimensions.mediumLarge, vertical = Dimensions.small),
horizontalArrangement = Arrangement.Center,
) {
Row(
modifier = Modifier
.background(
color = AppColors.primaryAccent.copy(alpha = 0.1f),
shape = RoundedCornerShape(6.dp)
)
.padding(horizontal = 10.dp, vertical = 4.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(6.dp),
) {
Icon(
imageVector = Icons.Default.Build,
contentDescription = "Tools enabled",
modifier = Modifier.size(10.dp),
tint = AppColors.primaryAccent,
)
Text(
text = "Tools enabled ($toolCount)",
style = AppTypography.caption2,
color = AppColors.primaryAccent,
)
}
}
}

@Composable
fun ModelSelectionPrompt(onSelectModel: () -> Unit) {
Surface(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import com.runanywhere.runanywhereai.domain.models.Conversation
import com.runanywhere.runanywhereai.domain.models.MessageAnalytics
import com.runanywhere.runanywhereai.domain.models.MessageModelInfo
import com.runanywhere.runanywhereai.domain.models.MessageRole
import com.runanywhere.runanywhereai.domain.models.ToolCallInfo
import com.runanywhere.sdk.public.extensions.LLM.ToolValue
import com.runanywhere.sdk.public.RunAnywhere
import com.runanywhere.sdk.public.events.EventBus
import com.runanywhere.sdk.public.events.LLMEvent
Expand All @@ -23,8 +25,19 @@ import com.runanywhere.sdk.public.extensions.generate
import com.runanywhere.sdk.public.extensions.generateStream
import com.runanywhere.sdk.public.extensions.isLLMModelLoaded
import com.runanywhere.sdk.public.extensions.loadLLMModel
import com.runanywhere.sdk.public.extensions.LLM.ToolCallingOptions
import com.runanywhere.sdk.public.extensions.LLM.ToolCallFormat
import com.runanywhere.sdk.public.extensions.LLM.RunAnywhereToolCalling
import com.runanywhere.runanywhereai.presentation.settings.ToolSettingsViewModel
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonNull
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.buildJsonArray
import kotlinx.serialization.json.buildJsonObject
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.filterIsInstance
Expand Down Expand Up @@ -202,7 +215,15 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
// Clear metrics from previous generation
tokensPerSecondHistory.clear()

if (currentState.useStreaming) {
// Check if tool calling is enabled and tools are registered
val toolViewModel = ToolSettingsViewModel.getInstance(app)
val useToolCalling = toolViewModel.toolCallingEnabled
val registeredTools = RunAnywhereToolCalling.getRegisteredTools()

if (useToolCalling && registeredTools.isNotEmpty()) {
Log.i(TAG, "🔧 Using tool calling with ${registeredTools.size} tools")
generateWithToolCalling(prompt, assistantMessage.id)
} else if (currentState.useStreaming) {
generateWithStreaming(prompt, assistantMessage.id)
} else {
generateWithoutStreaming(prompt, assistantMessage.id)
Expand All @@ -213,6 +234,90 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
}
}

/**
* Generate with tool calling support
* Matches iOS generateWithToolCalling pattern
*/
private suspend fun generateWithToolCalling(
prompt: String,
messageId: String,
) {
val startTime = System.currentTimeMillis()

try {
// Detect the appropriate tool call format based on loaded model
// Note: loadedModelName can be null if model state changes during generation
val modelName = _uiState.value.loadedModelName
if (modelName == null) {
Log.w(TAG, "⚠️ Tool calling initiated but model name is null, using default format")
}
val toolViewModel = ToolSettingsViewModel.getInstance(app)
val format = toolViewModel.detectToolCallFormat(modelName)

Log.i(TAG, "🔧 Tool calling with format: $format for model: ${modelName ?: "unknown"}")

// Create tool calling options
val toolOptions = ToolCallingOptions(
maxToolCalls = 3,
autoExecute = true,
temperature = 0.7f,
maxTokens = 1024,
format = format
)

// Generate with tools
val result = RunAnywhereToolCalling.generateWithTools(prompt, toolOptions)
val endTime = System.currentTimeMillis()

// Update the assistant message with the result
val response = result.text
updateAssistantMessage(messageId, response, null)

// Log tool calls and create tool call info
if (result.toolCalls.isNotEmpty()) {
Log.i(TAG, "🔧 Tool calls made: ${result.toolCalls.map { it.toolName }}")
result.toolResults.forEach { toolResult ->
Log.i(TAG, "📋 Tool result: ${toolResult.toolName} - success: ${toolResult.success}")
}

// Create ToolCallInfo from the first tool call and result
val firstToolCall = result.toolCalls.first()
val firstToolResult = result.toolResults.firstOrNull { it.toolName == firstToolCall.toolName }

val toolCallInfo = ToolCallInfo(
toolName = firstToolCall.toolName,
arguments = formatToolValueMapToJson(firstToolCall.arguments),
result = firstToolResult?.result?.let { formatToolValueMapToJson(it) },
success = firstToolResult?.success ?: false,
error = firstToolResult?.error,
)

updateAssistantMessageWithToolCallInfo(messageId, toolCallInfo)
}

// Create analytics
val analytics = createMessageAnalytics(
startTime = startTime,
endTime = endTime,
firstTokenTime = null,
thinkingStartTime = null,
thinkingEndTime = null,
inputText = prompt,
outputText = response,
thinkingText = null,
wasInterrupted = false,
)

updateAssistantMessageWithAnalytics(messageId, analytics)

} catch (e: Exception) {
Log.e(TAG, "Tool calling failed", e)
throw e
} finally {
_uiState.value = _uiState.value.copy(isGenerating = false)
}
}

/**
* Generate with streaming support and thinking mode
* Matches iOS streaming generation pattern
Expand Down Expand Up @@ -456,6 +561,23 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
_uiState.value = _uiState.value.copy(messages = updatedMessages)
}

private fun updateAssistantMessageWithToolCallInfo(
messageId: String,
toolCallInfo: ToolCallInfo,
) {
val currentMessages = _uiState.value.messages
val updatedMessages =
currentMessages.map { message ->
if (message.id == messageId) {
message.copy(toolCallInfo = toolCallInfo)
} else {
message
}
}

_uiState.value = _uiState.value.copy(messages = updatedMessages)
}

/**
* Create message analytics using app-local types
*/
Expand Down Expand Up @@ -724,6 +846,38 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
_uiState.value = _uiState.value.copy(error = null)
}

/**
* Format a ToolValue map to JSON string for display.
* Uses kotlinx.serialization for proper JSON escaping of special characters.
*/
private fun formatToolValueMapToJson(map: Map<String, ToolValue>): String {
val jsonObject = buildJsonObject {
map.forEach { (key, value) ->
put(key, formatToolValueToJsonElement(value))
}
}
return Json.encodeToString(JsonObject.serializer(), jsonObject)
}

/**
* Convert a ToolValue to the appropriate JsonElement type.
* Handles all ToolValue variants with proper JSON escaping.
*/
private fun formatToolValueToJsonElement(value: ToolValue): JsonElement {
return when (value) {
is ToolValue.StringValue -> JsonPrimitive(value.value)
is ToolValue.NumberValue -> JsonPrimitive(value.value)
is ToolValue.BoolValue -> JsonPrimitive(value.value)
is ToolValue.NullValue -> JsonNull
is ToolValue.ArrayValue -> buildJsonArray {
value.value.forEach { add(formatToolValueToJsonElement(it)) }
}
is ToolValue.ObjectValue -> buildJsonObject {
value.value.forEach { (k, v) -> put(k, formatToolValueToJsonElement(v)) }
}
}
}

companion object {
private const val TAG = "ChatViewModel"
}
Expand Down
Loading
Loading