Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
568 changes: 216 additions & 352 deletions README.md

Large diffs are not rendered by default.

17 changes: 16 additions & 1 deletion src/components/ModelLoaderWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ import { AppColors } from '../theme';
interface ModelLoaderWidgetProps {
title: string;
subtitle: string;
/**
* "Qwen3.5 0.8B Q4_K_M · Alibaba". Optional so a caller with no single model
* behind it (the voice pipeline loads three) can leave it off.
*/
modelCredit?: string;
icon: string;
accentColor: string;
isDownloading: boolean;
Expand All @@ -22,6 +27,7 @@ interface ModelLoaderWidgetProps {
export const ModelLoaderWidget: React.FC<ModelLoaderWidgetProps> = ({
title,
subtitle,
modelCredit,
accentColor,
isDownloading,
isLoading,
Expand All @@ -46,6 +52,9 @@ export const ModelLoaderWidget: React.FC<ModelLoaderWidgetProps> = ({

<Text style={styles.title}>{title}</Text>
<Text style={styles.subtitle}>{subtitle}</Text>
{modelCredit ? (
<Text style={[styles.modelCredit, { color: accentColor }]}>{modelCredit}</Text>
) : null}

{(isDownloading || isLoading) && (
<View style={styles.loadingContainer}>
Expand Down Expand Up @@ -125,9 +134,15 @@ const styles = StyleSheet.create({
fontSize: 14,
color: AppColors.textSecondary,
textAlign: 'center',
marginBottom: 32,
marginBottom: 8,
lineHeight: 20,
},
modelCredit: {
fontSize: 13,
fontWeight: '600',
textAlign: 'center',
marginBottom: 24,
},
loadingContainer: {
alignItems: 'center',
marginVertical: 24,
Expand Down
3 changes: 2 additions & 1 deletion src/screens/ChatScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import LinearGradient from 'react-native-linear-gradient';
import { RunAnywhere } from '@runanywhere/core';
import type { GenerationEvent, GenerationResult } from '@runanywhere/core';
import { AppColors } from '../theme';
import { useModelService } from '../services/ModelService';
import { useModelService, MODEL_CREDITS } from '../services/ModelService';
import { ChatMessageBubble, ChatMessage, ModelLoaderWidget } from '../components';

export const ChatScreen: React.FC = () => {
Expand Down Expand Up @@ -144,6 +144,7 @@ export const ChatScreen: React.FC = () => {
if (!modelService.isLLMLoaded) {
return (
<ModelLoaderWidget
modelCredit={MODEL_CREDITS.llm}
title="LLM Model Required"
subtitle="Download and load the language model to start chatting"
icon="chat"
Expand Down
3 changes: 2 additions & 1 deletion src/screens/SpeechToTextScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import LinearGradient from 'react-native-linear-gradient';
import { RunAnywhere, AudioInputs } from '@runanywhere/core';
import { ModelCategory } from '@runanywhere/proto-ts/model_types';
import { AppColors } from '../theme';
import { useModelService } from '../services/ModelService';
import { useModelService, MODEL_CREDITS } from '../services/ModelService';
import { ModelLoaderWidget, AudioVisualizer } from '../components';

// Native Audio Module - records in WAV format (16kHz mono) optimal for Whisper STT
Expand Down Expand Up @@ -191,6 +191,7 @@ export const SpeechToTextScreen: React.FC = () => {
if (!modelService.isSTTLoaded) {
return (
<ModelLoaderWidget
modelCredit={MODEL_CREDITS.stt}
title="STT Model Required"
subtitle="Download and load the speech recognition model"
icon="mic"
Expand Down
3 changes: 2 additions & 1 deletion src/screens/TextToSpeechScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import LinearGradient from 'react-native-linear-gradient';
import { RunAnywhere } from '@runanywhere/core';
import type { SpeechHandle } from '@runanywhere/core';
import { AppColors } from '../theme';
import { useModelService } from '../services/ModelService';
import { useModelService, MODEL_CREDITS } from '../services/ModelService';
import { ModelLoaderWidget } from '../components';

const SAMPLE_TEXTS = [
Expand Down Expand Up @@ -74,6 +74,7 @@ export const TextToSpeechScreen: React.FC = () => {
if (!modelService.isTTSLoaded) {
return (
<ModelLoaderWidget
modelCredit={MODEL_CREDITS.tts}
title="TTS Voice Required"
subtitle="Download and load the voice synthesis model"
icon="volume"
Expand Down
3 changes: 2 additions & 1 deletion src/screens/ToolCallingScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import LinearGradient from 'react-native-linear-gradient';
import { generateWithTools } from '@runanywhere/core';
import type { ToolCallingResult } from '@runanywhere/core';
import { AppColors } from '../theme';
import { useModelService } from '../services/ModelService';
import { useModelService, MODEL_CREDITS } from '../services/ModelService';
import { ModelLoaderWidget } from '../components';
import { DEMO_TOOLS, registerDemoTools } from '../utils/chatSampleTools';

Expand Down Expand Up @@ -138,6 +138,7 @@ export const ToolCallingScreen: React.FC = () => {
if (!modelService.isLLMLoaded) {
return (
<ModelLoaderWidget
modelCredit={MODEL_CREDITS.llm}
title="LLM Model Required"
subtitle="Download and load a language model to test tool calling"
icon="tools"
Expand Down
3 changes: 2 additions & 1 deletion src/screens/VisionScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
} from 'react-native';
import * as RNFS from 'react-native-fs';
import { AppColors } from '../theme';
import { useModelService } from '../services/ModelService';
import { useModelService, MODEL_CREDITS } from '../services/ModelService';
import { ModelLoaderWidget } from '../components';
import { VLMService } from '../services/VLMService';

Expand Down Expand Up @@ -103,6 +103,7 @@ export const VisionScreen: React.FC = () => {
if (!modelService.isVLMLoaded) {
return (
<ModelLoaderWidget
modelCredit={MODEL_CREDITS.vlm}
title="Vision Model Required"
subtitle="Download and load the vision-language model to describe images"
icon="vision"
Expand Down
42 changes: 32 additions & 10 deletions src/services/ModelService.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import React, { createContext, useContext, useState, useCallback } from 'react';
import { Platform } from 'react-native';

import { modelCredit } from './modelOrg';
import { RunAnywhere } from '@runanywhere/core';
import {
ModelCategory,
Expand All @@ -11,12 +13,32 @@ import {
// Model IDs - matching sample app model registry
// See: runanywhere-sdks/examples/react-native/RunAnywhereAI/src/services/ModelCatalogBootstrap.ts
export const MODEL_IDS = {
llm: 'lfm2-350m-q8_0', // LiquidAI LFM2 - fast and efficient
llm: 'qwen3.5-0.8b-q4_k_m', // Qwen3.5 - smallest current-generation chat model
vlm: 'smolvlm-500m-instruct-q8_0', // SmolVLM - ultra-light vision model
stt: 'sherpa-onnx-whisper-tiny.en',
tts: 'vits-piper-en_US-lessac-medium',
} as const;

/** Display names, kept beside the ids they belong to. */
export const MODEL_NAMES = {
llm: 'Qwen3.5 0.8B Q4_K_M',
vlm: 'SmolVLM 500M Instruct',
stt: 'Sherpa Whisper Tiny (ONNX)',
tts: 'Piper TTS (US English - Medium)',
} as const;

/**
* "Qwen3.5 0.8B Q4_K_M · Alibaba", for the loader screens. A starter that only
* says "the language model" leaves the reader with no idea what is about to be
* downloaded or who published it.
*/
export const MODEL_CREDITS = {
llm: modelCredit(MODEL_IDS.llm, MODEL_NAMES.llm),
vlm: modelCredit(MODEL_IDS.vlm, MODEL_NAMES.vlm),
stt: modelCredit(MODEL_IDS.stt, MODEL_NAMES.stt),
tts: modelCredit(MODEL_IDS.tts, MODEL_NAMES.tts),
} as const;

/**
* `ModelInfo.isDownloaded` was deleted from the IDL; `registryStatus` is the
* single downloaded-ness signal now.
Expand Down Expand Up @@ -334,22 +356,22 @@ export const ModelServiceProvider: React.FC<ModelServiceProviderProps> = ({ chil
* runanywhere-sdks/examples/react-native/RunAnywhereAI/src/services/ModelCatalogBootstrap.ts
*/
export const registerDefaultModels = async () => {
// LLM Model - LiquidAI LFM2 350M (fast, efficient, great for mobile)
// LLM Model - Qwen3.5 0.8B, the smallest current-generation chat model.
await RunAnywhere.models.register({
id: MODEL_IDS.llm,
name: 'LiquidAI LFM2 350M Q8_0',
url: 'https://huggingface.co/LiquidAI/LFM2-350M-GGUF/resolve/main/LFM2-350M-Q8_0.gguf',
name: 'Qwen3.5 0.8B Q4_K_M',
url: 'https://huggingface.co/unsloth/Qwen3.5-0.8B-GGUF/resolve/main/Qwen3.5-0.8B-Q4_K_M.gguf',
framework: InferenceFramework.INFERENCE_FRAMEWORK_LLAMA_CPP,
memoryRequirementBytes: 400_000_000,
memoryRequirementBytes: 900_000_000,
});

// Also add SmolLM2 as alternative smaller model
// A smaller alternative for low-memory devices.
await RunAnywhere.models.register({
id: 'smollm2-360m-q8_0',
name: 'SmolLM2 360M Q8_0',
url: 'https://huggingface.co/prithivMLmods/SmolLM2-360M-GGUF/resolve/main/SmolLM2-360M.Q8_0.gguf',
id: 'lfm2.5-230m-q4_k_m',
name: 'LiquidAI LFM2.5 230M Q4_K_M',
url: 'https://huggingface.co/LiquidAI/LFM2.5-230M-GGUF/resolve/main/LFM2.5-230M-Q4_K_M.gguf',
framework: InferenceFramework.INFERENCE_FRAMEWORK_LLAMA_CPP,
memoryRequirementBytes: 500_000_000,
memoryRequirementBytes: 190_000_000,
});

// VLM Model - SmolVLM 500M (ultra-lightweight vision-language model, ~600MB)
Expand Down
72 changes: 72 additions & 0 deletions src/services/modelOrg.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/**
* The publisher behind a model, so the loader can say who made the thing it is
* about to download instead of just "the language model".
*
* The rule table is a hand-kept copy of the same table in iOS
* (`ModelOrgCatalog`), Android (`ModelTaxonomy`), Web, Electron and Flutter.
* Five copies is a known cost; the alternative is a field on the catalog row,
* which is a commons change. Keep them in step when a family is added.
*/

export interface ModelOrg {
/** Stable key, e.g. "nvidia". */
key: string;
/** Consumer-facing name, e.g. "NVIDIA". */
name: string;
}

/**
* Ordered matchers against the lowercased "id + name" haystack. First match
* wins, so a specific publisher precedes the family it would otherwise be
* swallowed by: NVIDIA before Meta so Nemotron stays NVIDIA, DeepSeek before
* Alibaba so the R1 Qwen distills stay DeepSeek.
*/
const ORG_MATCHERS: ReadonlyArray<{ key: string; name: string; test: RegExp }> = [
{
key: 'nvidia',
name: 'NVIDIA',
test: /nemotron|nemoguard|cosmos|canary|parakeet|nv[_-]embed|nv_rerank|nvidia|sortformer/,
},
{ key: 'deepseek', name: 'DeepSeek', test: /deepseek/ },
{ key: 'prism', name: 'Prism', test: /bonsai|prismml|prism-?ml/ },
{ key: 'deepgrove', name: 'Deepgrove', test: /maple/ },
{ key: 'ibm', name: 'IBM', test: /granite/ },
// `fara` rides with Microsoft's `phi`: Fara1.5 ships mirrored under our own HF
// org, so the catalog row names no upstream publisher. Filing it by its own
// name beats guessing one into a UI label.
{ key: 'microsoft', name: 'Microsoft', test: /\bphi\b|fara/ },
{ key: 'google', name: 'Google', test: /gemma|embeddinggemma|siglip/ },
// Muse Glimmer is Meta's, per the catalog row's own name.
{ key: 'meta', name: 'Meta', test: /llama|muse-glimmer|muse_glimmer/ },
{ key: 'alibaba', name: 'Alibaba', test: /qwen/ },
{ key: 'liquid', name: 'Liquid AI', test: /lfm2/ },
{ key: 'mistral', name: 'Mistral AI', test: /mistral|ministral/ },
{ key: 'hugging-face', name: 'Hugging Face', test: /smollm|smolvlm/ },
{ key: 'openai', name: 'OpenAI', test: /whisper/ },
{ key: 'zhipu', name: 'Zhipu AI', test: /\bglm\b|glm-/ },
{
key: 'open-source',
name: 'Open source',
test: /internvl|moonshine|melo|kokoro|kitten|piper|vits|silero|vad|minilm|supertonic|segformer/,
},
];

const FALLBACK_ORG: ModelOrg = { key: 'open-source', name: 'Open source' };

/**
* The publisher for one model. Never throws; an unrecognised name reads as
* community rather than guessing a company.
*/
export function modelOrg(id: string, name: string): ModelOrg {
const haystack = `${id} ${name}`.toLowerCase();
const match = ORG_MATCHERS.find((org) => org.test.test(haystack));
return match ?? FALLBACK_ORG;
}

/**
* "Qwen3.5 0.8B Q4_K_M · Alibaba" — what the loader shows so a reader knows
* what is about to land on their device and who made it.
*/
export function modelCredit(id: string, name: string): string {
return `${name} · ${modelOrg(id, name).name}`;
}