npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@tcbs/react-native-ondevice-ai

v2.0.1

Published

React Native on-device model management and multimodal Gemma LiteRT-LM inference

Readme

React Native On-Device AI — private multimodal AI for iOS and Android

@tcbs/react-native-ondevice-ai

Production-oriented React Native bindings for Google LiteRT-LM 0.16.0, generic model-file management, and multimodal Gemma inference. The package runs inference locally and does not upload prompts or media.

Highlights

  • Text, image, and audio generation with one conversational native engine
  • Google LiteRT-LM 0.16.0 on Android and the official 0.16.0 iOS XCFramework
  • CPU/GPU backend selection plus Android NPU and Google Tensor backends
  • Sampling, deterministic seeds, output/context limits, LoRA, thinking, repetition controls, no-repeat n-grams, and constrained responses
  • Regex and JSON Schema output, reasoning channels, and parsed tool calls
  • Speculative-decoding capability checks and experimental decoding controls
  • Explicit conversation reset, model unload, token count, and runtime status
  • Download recovery, storage checks, import/export, speech/audio helpers, OCR, and Apple system-language-model integration

Compatibility

| Requirement | Supported | | --- | --- | | React Native | 0.72 or newer (legacy Native Module bridge) | | React | 18 or newer | | Android | API 24+, compile SDK 34+ | | iOS | 15.0+ | | Node.js | 18+ |

The package works in New Architecture applications through React Native's interop layer, but it is not yet implemented as a TurboModule.

AI-assisted development

This repository is prepared for AI-assisted coding. Start with AGENTS.md, which is the canonical instruction file for coding agents. Supporting context is split into docs/ARCHITECTURE.md, docs/API.md, and docs/TROUBLESHOOTING.md.

CLAUDE.md, .github/copilot-instructions.md, and llms.txt point tools to the same sources so that project rules do not drift between assistants.

Google's 0.16.0 iOS simulator slice is arm64-only. On an Intel Mac, use a physical iOS device; on Apple silicon, exclude x86_64 if an existing project forces universal simulator architectures.

Installation

npm install @tcbs/react-native-ondevice-ai

For iOS, install pods after adding the package:

cd ios && pod install

The Android library manifest contributes INTERNET, ACCESS_NETWORK_STATE, and RECORD_AUDIO. The consuming iOS app must provide an NSMicrophoneUsageDescription. Model downloads and inference should be tested on a physical device before shipping; simulator performance is not representative.

Current scope

  • Android native download manager integration
  • iOS native URLSession download integration
  • Storage preflight check
  • Download progress polling
  • Finalize/cancel model download
  • Model file metadata lookup
  • Generic import, export, and deletion by model filename
  • Gemma text, image, and audio inference through LiteRT-LM 0.16.0
  • Advanced sampling, penalties, thinking, constrained output, LoRA, and experimental decoding controls
  • Conversation reset, engine unload, token count, and model capability lookup
  • Cross-platform 16 kHz mono WAV recording helpers

Design boundary

This package intentionally keeps model selection outside the package.

  • The package is responsible for native download/file lifecycle primitives.
  • The app is responsible for model policy constants such as:
    • model URL (for example Hugging Face artifact URL),
    • expected model size in bytes,
    • storage safety buffer.

This keeps the package reusable across apps and model variants while letting each app choose its own model/version policy.

OpenAI-compatible client

Version 2 adds an additive, direct-bridge client for common local Chat Completions usage:

import {OpenAICompatible} from '@tcbs/react-native-ondevice-ai';

const client = new OpenAICompatible({model: 'gemma-4-e2b'});
const response = await client.chat.completions.create({
  messages: [{role: 'user', content: 'Hello'}],
});

The client runs locally through the existing native LiteRT-LM bridge; it does not contact OpenAI servers. It supports local text and file-based vision requests, streaming, cancellation, model listing, supported sampling options, and JSON output. HTTP/data-URL images, stop, and multiple completions are rejected explicitly. Tool schemas can be forwarded and model-generated tool calls can be returned for app-side dispatch, but automatic JavaScript tool execution is not provided. System messages and new user messages are supported through the native conversation API. Full OpenAI-compatible replay of prior assistant and tool messages requires native conversation-history support that LiteRT-LM does not currently expose. The original low-level package APIs remain unchanged.

The facade is a JavaScript client, not an HTTP server: it does not expose /v1/chat/completions, listen on a port, or contact OpenAI/Azure. It does not depend on the openai npm package. The named import above is for the new client; the legacy default export remains available for existing consumers.

Client options

const client = new OpenAICompatible({
  model: 'gemma-4-e2b',
  modelFileName: 'gemma_4_e2b.litertlm',
  defaultGenerationOptions: {temperature: 0.7, maxTokens: 256},
});

model is the local model identifier. modelFileName identifies the local model file used by the native bridge. Request options override these defaults.

OpenAI-style image inference

Images must be readable local files and the selected model must support vision:

const response = await client.chat.completions.create({
  messages: [{
    role: 'user',
    content: [
      {type: 'text', text: 'Describe this image.'},
      {type: 'image_url', image_url: {url: 'file:///absolute/path/to/image.jpg'}},
    ],
  }],
});

Local paths and file:// URIs are supported. HTTP URLs and base64/data URLs are currently rejected.

OpenAI-style streaming

const stream = await client.chat.completions.create({
  stream: true,
  messages: [{role: 'user', content: 'Tell me a short story.'}],
});

for await (const chunk of stream) {
  console.log(chunk.choices[0]?.delta?.content ?? '');
}

Cancel with AbortSignal or client.interrupt().

OpenAI-style tool calls

Tool schemas can be forwarded to a tool-capable local model:

const response = await client.chat.completions.create({
  messages: [{role: 'user', content: 'What is the weather in Paris?'}],
  tools: [{
    type: 'function',
    function: {
      name: 'get_weather',
      description: 'Get current weather.',
      parameters: {
        type: 'object',
        properties: {city: {type: 'string'}},
        required: ['city'],
      },
    },
  }],
});

const toolCall = response.choices[0]?.message.tool_calls?.[0];
// Execute the function in application code and send a later tool message.

Automatic JavaScript callback execution is not provided. Full replay of prior assistant/tool history requires native conversation-history support that LiteRT-LM does not currently expose. OpenAI-style input_audio base64 content is also unsupported; use generateTextWithAudio with a local audio file.

Provider-portable business logic

import OpenAI from 'openai';
import {OpenAICompatible, type OpenAIMessage} from '@tcbs/react-native-ondevice-ai';

const cloudClient = new OpenAI({baseURL, apiKey});
const localClient = new OpenAICompatible({model: 'gemma-4-e2b'});

// A deliberately small application-owned interface. The official OpenAI
// client has richer overloaded TypeScript declarations, but both clients
// support this common operation at runtime.
type PortableClient = {
  chat: {
    completions: {
      create: (request: {model: string; messages: OpenAIMessage[]}) => Promise<unknown>;
    };
  };
};

async function ask(client: PortableClient, model: string, messages: OpenAIMessage[]) {
  return client.chat.completions.create({model, messages});
}

// In production, pass provider clients through a small application adapter
// that satisfies PortableClient, or define the interface using the official
// OpenAI SDK types in the cloud build.

Use a shared application abstraction around chat.completions.create when supporting both cloud and local providers.

The package owns the native operations in this lifecycle:

check storage → download → monitor/restore → finalize → infer → export/import → delete

The consuming application owns the model URL, expected size, filename, UI, and downloaded-artifact integrity policy. The Gemma model itself is not bundled.

LiteRT-LM model lifecycle

Defaults and current model constraint

  • Default model filename: gemma_4_e2b.litertlm.
  • Default download policy: Wi-Fi only.
  • Default storage safety buffer: 250 MiB.
  • Default export directory name: SubraAI.
  • generateText, generateTextWithImage, and generateTextWithAudio currently load only gemma_4_e2b.litertlm. Although lifecycle APIs accept other filenames, move or import the inference model under this default name.
  • Finalizing or importing over an existing filename replaces that file.
  • Model files live in application-private storage. Uninstalling the application removes them.

Complete download and restore example

import {
  cancelDownload,
  checkStorage,
  finalizeModelDownload,
  getActiveDownloadIdForFile,
  getDownloadStatus,
  getModelFileInfo,
  startModelDownload,
} from '@tcbs/react-native-ondevice-ai';

const fileName = 'gemma_4_e2b.litertlm';

export async function ensureModel(url: string, modelBytes: number) {
  const existing = await getModelFileInfo(fileName);
  if (existing.exists && existing.sizeBytes > 0) return existing;

  const storage = await checkStorage({ requiredBytes: modelBytes });
  if (!storage.hasEnoughSpace) throw new Error('Insufficient model storage');

  // Reattach after an app restart, or start a new native download.
  const restoredId = await getActiveDownloadIdForFile(fileName);
  const downloadId = restoredId ?? await startModelDownload({
    url,
    fileName,
    wifiOnly: true,
  });

  while (true) {
    const snapshot = await getDownloadStatus(downloadId);
    if (snapshot.status === 'Successful') break;
    if (snapshot.status === 'NotFound' || snapshot.status.startsWith('Failed')) {
      throw new Error(`Model download failed: ${snapshot.status}`);
    }
    await new Promise(resolve => setTimeout(resolve, 1000));
  }

  if (!await finalizeModelDownload(fileName)) {
    throw new Error('Downloaded model could not be finalized');
  }
  return getModelFileInfo(fileName);
}

export async function cancelModelDownload() {
  const id = await getActiveDownloadIdForFile(fileName);
  return id === null ? false : cancelDownload(id);
}

getActiveDownloadId() returns the most recently persisted download and exists for compatibility. Prefer getActiveDownloadIdForFile() when managing more than one filename. A returned ID is not proof that a task is still running; always call getDownloadStatus().

Text, image, and audio inference

import {
  generateText,
  benchmarkLiteRTLM,
  generateTextWithAudio,
  generateTextWithImage,
  pickAudioFile,
  startAudioRecording,
  stopAudioRecording,
} from '@tcbs/react-native-ondevice-ai';

const answer = await generateText('Explain on-device inference briefly.', {
  temperature: 0.7,
  topK: 40,
  topP: 0.95,
  maxTokens: 512,
});
console.log(answer.text);

const description = await generateTextWithImage(
  'Describe this image.',
  'file:///absolute/path/to/image.jpg',
  { temperature: 0.2 },
);
console.log(description.text);

// iOS normalizes supported source images to an upright, bounded JPEG before
// passing them to LiteRT-LM. Invalid image files reject without entering the
// native vision decoder.

await startAudioRecording();
// Stop in response to the user's action. Keep each model input under the
// audio-duration limit documented for the selected model.
const recording = await stopAudioRecording();
const transcript = await generateTextWithAudio(
  'Transcribe this audio accurately.',
  recording.uri,
);
console.log(transcript.text, recording.durationMs);

const selectedAudio = await pickAudioFile();
console.log(selectedAudio.uri, selectedAudio.durationMs);

Inference defaults are temperature: 0.7, topK: 40, and topP: 0.95. On iOS, the native bridge normalizes seeds outside the signed 32-bit range before Google's Swift wrapper receives them, preventing an integer-overflow trap. This makes timestamp-derived JavaScript seeds safe to pass. Changing engine or conversation options recreates the cached native conversation. maxTokens limits output and seed controls the sampler. Empty text prompts resolve to { text: '' }. Image inference requires a local readable file and a vision-capable LiteRT-LM model. Audio inference requires a local audio file and an audio-capable model. The recording helpers produce 16 kHz, mono, 16-bit PCM WAV files in temporary/cache storage. Use cancelAudioRecording() to discard an active recording.

Pass chunkDurationMs to startAudioRecording() to receive finalized temporary WAV chunks through subscribeAudioRecordingChunk(). Chunk duration is clamped to 20–28 seconds on Android. Audio recording chunks are currently Android-only; iOS rejects chunkDurationMs explicitly.

The native module retains one engine/conversation per platform module instance. Treat inference calls as serial operations; cancelTextGeneration() cancels the active call where supported, and generateTextStream() delivers incremental native chunks. The package does not provide a concurrency queue.

Advanced LiteRT-LM 0.16 generation

All three generation functions accept the same options. Only enable a modality backend when the selected .litertlm model contains that modality.

import {
  generateText,
  getLiteRTLMCapabilities,
  getLiteRTLMRuntimeInfo,
  resetConversation,
  unloadModel,
} from '@tcbs/react-native-ondevice-ai';

const capabilities = await getLiteRTLMCapabilities();

const result = await generateText('Return a compact hydration plan.', {
  backend: 'cpu',
  visionBackend: 'disabled',
  audioBackend: 'disabled',
  maxContextTokens: 4096,
  maxTokens: 256,
  temperature: 0.4,
  topK: 40,
  topP: 0.9,
  seed: 7,
  systemPrompt: 'Be concise and use metric units.',
  repetitionPenalty: 1.1,
  presencePenalty: 0.1,
  frequencyPenalty: 0.1,
  penaltyWindowSize: 256,
  noRepeatNgramSize: 3,
  noRepeatNgramWindowSize: 256,
  thinking: {enabled: true, tokenBudget: 128},
  enableSpeculativeDecoding: capabilities.speculativeDecoding,
  filterChannelContentFromKvCache: true,
  responseFormat: {
    type: 'json_schema',
    schema: {
      type: 'object',
      properties: {summary: {type: 'string'}},
      required: ['summary'],
    },
  },
});

console.log(result.text, result.channels, result.toolCalls);
console.log(await getLiteRTLMRuntimeInfo());
await resetConversation(); // retain engine, clear chat/KV state
await unloadModel();       // release conversation and model memory

const perf = await benchmarkLiteRTLM({
  backend: 'cpu',
  prefillTokens: 256,
  decodeTokens: 128,
});
console.log(perf.lastPrefillTokensPerSecond, perf.lastDecodeTokensPerSecond);

For incremental UI updates, use the event-backed streaming wrapper. Each call has its own request ID internally, removes its listener on completion/error, and returns the final accumulated response as a promise.

import {generateTextStream} from '@tcbs/react-native-ondevice-ai';

const stream = generateTextStream(
  'Write three short reminders.',
  chunk => console.log('delta:', chunk.text, chunk.channels),
  {maxTokens: 128},
);

// Optional: await stream.cancel();
const complete = await stream.result;
console.log('complete:', complete.text);

Backend values are cpu, gpu, npu, and google-tensor; NPU and Google Tensor are Android-only. iOS retries CPU if GPU initialization fails. LoRA is configured with loraPath/audioLoraPath and the corresponding rank options. Gemma 4 visual token budgets are 70, 140, 280, 560, or 1120.

responseFormat accepts {type: 'regex', pattern} or {type: 'json_schema', schema}. Enable it only for a model that supports constrained decoding. suppressTokens is supported on iOS; it is deliberately ignored on Android 0.16.0 because the upstream JNI path can terminate the process instead of returning an error. Tool declarations contain native callbacks and therefore cannot be registered from JavaScript in this bridge; model-emitted tool calls are returned in result.toolCalls for app-side dispatch. Automatic tool calling defaults to false; leave it disabled for that manual workflow.

maxImages is supported by the Android 0.16.0 engine configuration. Google's iOS 0.16.0 wrapper does not expose an equivalent setting, so iOS ignores it.

The experimental options mirror Google flags and may change upstream: enableSpeculativeDecoding, enableConversationConstrainedDecoding, and filterChannelContentFromKvCache. Check capabilities before forcing speculative decoding; requesting it for an unsupported model fails engine load.

Export and import

import {
  exportModelToDownloads,
  getExportedModelInfo,
  importModelFromDownloads,
  openExportedModelInFiles,
  subscribeTransferProgress,
} from '@tcbs/react-native-ondevice-ai';

const unsubscribe = subscribeTransferProgress(event => {
  console.log(event.operation, event.progressPercent);
});

try {
  const exported = await exportModelToDownloads({
    fileName: 'gemma_4_e2b.litertlm',
    exportDirName: 'MyModels',
  });
  console.log(exported.uri, exported.sizeBytes);

  const info = await getExportedModelInfo({
    fileName: 'gemma_4_e2b.litertlm',
    exportDirName: 'MyModels',
  });
  if (info?.exists) await openExportedModelInFiles({
    fileName: 'gemma_4_e2b.litertlm',
    exportDirName: 'MyModels',
  });

  const imported = await importModelFromDownloads({
    fileName: 'gemma_4_e2b.litertlm',
    exportDirName: 'MyModels',
  });
  console.log(imported.path, imported.sizeBytes);
} finally {
  unsubscribe();
}

Platform behavior differs:

| Operation | Android | iOS | | --- | --- | --- | | Export | Copies to Downloads using MediaStore (modern Android) or legacy Downloads | Presents the system share sheet with a temporary copy | | Import | Looks in the requested Downloads folder, then opens a picker if needed | Always presents the system document picker | | Progress events | Emitted while native copy operations run | Not currently emitted | | exportDirName | Selects the Downloads subdirectory | Used only by exported-file lookup helpers; share-sheet destination is user-controlled | | Open exported file | Opens Downloads/file handler | Presents document interaction options |

Only one document-picker import can be active at a time. Cancelling the picker rejects with import_cancelled. Import/export does not validate the file as a LiteRT-LM model; validate its size and a trusted cryptographic checksum in the application before inference.

On iOS, completing the share sheet does not create a package-managed exported copy. Consequently, getExportedModelInfo() and openExportedModelInFiles() may report no exported file after a share-sheet export; the destination selected by the user is outside the package's private storage. Use the share sheet itself as the authoritative iOS export workflow.

File inspection and deletion

import { deleteModel, getModelFileInfo } from '@tcbs/react-native-ondevice-ai';

const info = await getModelFileInfo('gemma_4_e2b.litertlm');
if (info.exists) {
  console.log(info.path, info.sizeBytes);
  await deleteModel('gemma_4_e2b.litertlm');
}

deleteModel() is idempotent for downloaded/imported files. It cannot remove a resource embedded in the application bundle.

API reference

OpenAI-compatible client

| API | Result | Notes | | --- | --- | --- | | new OpenAICompatible(options?) | client | Named export; local direct-bridge client | | client.chat.completions.create(request) | completion or async iterable | Supports local text, system/user messages, streaming, local image paths, JSON output, and tool-call mapping | | client.models.list() | model list | Reports the configured locally installed model | | client.models.retrieve(id) | model | Resolves a configured installed local model |

The facade is OpenAI-style JavaScript compatibility, not an OpenAI HTTP endpoint. Image image_url values must be readable local paths or file:// URIs. HTTP/data URLs, stop, and multiple completions are unsupported. Tool schemas and model-generated calls are supported for app-side dispatch; the facade does not automatically execute JavaScript functions.

Native and lifecycle APIs

| API | Result | Notes | | --- | --- | --- | | checkStorage({requiredBytes, safetyBufferBytes?}) | StorageCheckResult | requiredBytes in the result includes the safety buffer | | startModelDownload({url, wifiOnly?, fileName?}) | download ID | Persists the ID natively for restoration | | getDownloadStatus(downloadId) | DownloadSnapshot | Status includes Pending, Downloading, Paused, Successful, Failed(...), or NotFound | | getActiveDownloadId() | ID or null | Most recently persisted download | | getActiveDownloadIdForFile(fileName) | ID or null | Preferred restoration API | | finalizeModelDownload(fileName?) | boolean | Copies/moves staged download into private model storage | | cancelDownload(downloadId) | boolean | Cancels the native download task | | getModelFileInfo(fileName?) | ModelFileInfo | Returns existence, size, and private path | | deleteModel(fileName?) | boolean | Removes downloaded/imported model | | exportModelToDownloads(options?) | ExportResult | Downloads on Android; share sheet on iOS | | importModelFromDownloads(options?) | ModelFileInfo | May present a native picker | | getExportedModelInfo(options?) | info or null | Returns null only when unsupported by the native build | | openExportedModelInFiles(options?) | boolean | Presents the platform file UI | | subscribeTransferProgress(listener) | unsubscribe function | Copy progress is currently Android-only | | getCurrentNetworkClass() | network classification | Returns a conservative fallback if native support is unavailable | | generateText(prompt, options?) | GemmaGenerateTextResult | Text generation with LiteRT-LM 0.16 controls | | generateTextStream(prompt, onChunk, options?) | GemmaGenerationStream | Incremental chunks, final result promise, and cancellation | | generateTextWithImage(prompt, imagePath, options?) | GemmaGenerateTextResult | Requires local image and multimodal model | | generateTextWithAudio(prompt, audioPath, options?) | GemmaGenerateTextResult | Requires local audio and audio-capable model | | cancelTextGeneration() | boolean | Requests cancellation of active inference | | resetConversation() | boolean | Clears chat/KV state and retains the loaded engine | | unloadModel() | boolean | Releases the conversation and engine | | getLiteRTLMCapabilities(fileName?) | LiteRTLMCapabilities | Reads model capabilities without loading inference | | getLiteRTLMRuntimeInfo() | LiteRTLMRuntimeInfo | Returns 0.16.0 version, load state, and token count | | benchmarkLiteRTLM(options?) | LiteRTLMBenchmarkResult | Experimental prefill/decode performance benchmark | | startAudioRecording(options?) | boolean | Records 16 kHz mono WAV audio; Android can emit 20–28 second chunks with chunkDurationMs | | stopAudioRecording() | {uri, durationMs, hasSpeech} | Returns a temporary/cache file URI and speech-level detection | | cancelAudioRecording() | boolean | Stops and deletes the active recording | | subscribeAudioRecordingChunk(listener) | unsubscribe function | Receives finalized Android WAV chunks; no chunks are emitted on iOS | | pickAudioFile() | {uri, durationMs} | Copies a user-selected audio file into temporary/cache storage | | recognizeTextInImage(imagePath) | {text, lineCount} | Native on-device OCR | | isImageTextRecognitionAvailable() | availability | OCR availability and languages | | getSystemLanguageModelAvailability() | availability | Apple system model status; unavailable elsewhere | | generateTextWithSystemLanguageModel(prompt) | {text} | Apple Foundation Models where available | | speakText(text) / stopSpeaking() | boolean | Native text-to-speech controls |

Promise rejection codes

Native failures reject with a code and platform error message. Handle at least:

  • storage_check_failed
  • download_start_failed, download_status_failed, download_finalize_failed, download_cancel_failed
  • active_download_read_failed
  • model_info_failed, model_delete_failed, model_not_ready
  • generate_text_failed, generate_image_text_failed, image_not_found
  • export_failed, import_failed, import_cancelled
  • exported_model_info_failed, open_exported_failed
  • network_info_failed

React Native exposes the native code on the rejected error as error.code. Messages can differ by platform and should not be used as stable identifiers.

iOS setup

The pod vendors Google's official 0.16.0 CLiteRTLM.xcframework and its Swift wrapper sources, so the app must not add the upstream Swift package separately. The deployment target is iOS 15.0 or newer.

Implementation example (app side)

import {
  checkStorage,
  startModelDownload,
  getDownloadStatus,
  finalizeModelDownload,
  getModelFileInfo,
} from '@tcbs/react-native-ondevice-ai';

const GEMMA_MODEL_URL =
  'https://huggingface.co/litert-community/gemma-4-E2B-it-litert-lm/resolve/main/gemma-4-E2B-it.litertlm';
const REQUIRED_MODEL_BYTES = 2_581_242_684;
const SAFETY_BUFFER_BYTES = 250 * 1024 * 1024;

export async function downloadGemmaModel() {
  const storage = await checkStorage({
    requiredBytes: REQUIRED_MODEL_BYTES,
    safetyBufferBytes: SAFETY_BUFFER_BYTES,
  });

  if (!storage.hasEnoughSpace) {
    throw new Error('INSUFFICIENT_STORAGE');
  }

  const downloadId = await startModelDownload({
    url: GEMMA_MODEL_URL,
    wifiOnly: true,
    fileName: 'gemma_4_e2b.litertlm',
  });

  let status = 'Pending';
  while (status === 'Pending' || status === 'Downloading' || status === 'Paused') {
    const snapshot = await getDownloadStatus(downloadId);
    status = snapshot.status;
    // update UI with snapshot.progressPercent / snapshot.downloadedBytes / snapshot.totalBytes
    await new Promise(resolve => setTimeout(resolve, 1000));
  }

  if (status !== 'Successful') {
    throw new Error(`DOWNLOAD_FAILED: ${status}`);
  }

  const saved = await finalizeModelDownload('gemma_4_e2b.litertlm');
  if (!saved) {
    throw new Error('FINALIZE_FAILED');
  }

  const modelInfo = await getModelFileInfo('gemma_4_e2b.litertlm');
  if (!modelInfo.exists) {
    throw new Error('MODEL_NOT_FOUND');
  }

  return modelInfo;
}

Notes

  • Default model file name: gemma_4_e2b.litertlm
  • Download APIs accept arbitrary model url and fileName values.
  • A model embedded in the application cannot be permanently deleted at runtime; only downloaded/imported copies can be removed.

iOS LiteRT-LM linking

generateText on iOS is provided by the native react-native-ondevice-ai pod.

Important

Direct SwiftPM LiteRTLM linking can fail with: The package product 'LiteRTLM' cannot be used as a dependency ... because it uses unsafe build flags.

This package avoids that by embedding the LiteRT-LM Swift wrapper sources in the pod and linking the vendored CLiteRTLM.xcframework directly through CocoaPods.

After upgrading, run pod install. If the build requests an unavailable x86_64 simulator slice, remove a project-wide architecture override or set EXCLUDED_ARCHS[sdk=iphonesimulator*] = x86_64; the official 0.16.0 simulator framework contains arm64 only.

Runtime verification checklist

After launching app in debug and sending a chat message:

  • You should see app log:
    • [SubraAI][Inference] response_native ...
  • You should not see:
    • [SubraAI][Inference] response_fallback ... reason: 'NATIVE_GENERATION_UNAVAILABLE_OR_EMPTY'

Troubleshooting

  • If Xcode still shows LiteRTLM under Package Dependencies for the app or Pods target, remove it and reinstall pods.

Package size and bundled artifacts

The package includes Google's LiteRT-LM iOS XCFramework. Model weights are not bundled; applications download or import their chosen .litertlm model.

Before redistributing this package, maintainers must verify that the versions of LiteRT-LM and the vendored XCFramework remain compatible with their upstream licenses. See THIRD_PARTY_NOTICES.md.

Maintainer release checklist

npm ci
npm run verify
npm pack --dry-run

Also install the resulting .tgz in a clean React Native example application, build Android and iOS release configurations, and run download, Gemma text/image generation, cancellation/restoration, and import/export on physical devices. Do not publish when any of those checks are skipped.

Developer

Created and maintained by Subrata Kumar Das.