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

expo-ai-kit

v0.12.0

Published

On-device AI for Expo — run Gemma 4, Apple Foundation Models & ML Kit locally: streaming, structured output, tool calling, embeddings & RAG, Vercel AI SDK provider, zero API keys

Readme

expo-ai-kit

On-device AI for Expo & React Native — run LLMs locally. No API keys, no cloud, no cost.

npm version License: MIT

Runs Apple Foundation Models (iOS 26+), ML Kit (Android), and downloadable Gemma 4 (E2B / E4B, iOS + Android via LiteRT-LM) — with streaming, structured output, tool calling, embeddings & on-device RAG, cancellation, and runtime model switching, all on-device.

New in 0.11: a Vercel AI SDK provider — use generateText, streamText, and embed from the AI SDK with on-device models via expo-ai-kit/ai. Same useChat-style code you'd write for OpenAI, no API key.

Install

npx expo install expo-ai-kit

Bare RN: run npx pod-install. Android needs minSdkVersion 26. Requires Expo SDK 54+.

Text

import { isAvailable, sendMessage, streamMessage } from 'expo-ai-kit';

if (await isAvailable()) {
  const { text } = await sendMessage([{ role: 'user', content: 'Capital of France?' }]);

  const { promise, stop } = streamMessage(
    [{ role: 'user', content: 'Write a short story' }],
    (e) => setText(e.accumulatedText),
  );
  await promise; // stop() to cancel
}

messages is { role: 'system' | 'user' | 'assistant'; content: string }[]. On-device models are stateless — pass the full history each call.

Structured output

Get a typed object back instead of a string. Pass a JSON Schema; expo-ai-kit prompts the model, extracts the JSON (tolerating prose and code fences), validates it against the schema, and repairs on a mismatch. Works on every backend.

import { generateObject } from 'expo-ai-kit';

type Recipe = { title: string; minutes: number; ingredients: string[] };

const { object } = await generateObject<Recipe>(
  [{ role: 'user', content: 'A quick weeknight pasta.' }],
  {
    type: 'object',
    properties: {
      title: { type: 'string' },
      minutes: { type: 'integer' },
      ingredients: { type: 'array', items: { type: 'string' } },
    },
    required: ['title', 'minutes', 'ingredients'],
  },
);

object.title; // typed Recipe

Throws INFERENCE_FAILED if the model can't produce schema-valid JSON after the repair attempts (maxRepairAttempts, default 2). Keep schemas small and shallow for best results.

Tool calling

Let the model call functions you provide. It proposes a call, expo-ai-kit validates the arguments against the tool's schema, runs your execute, feeds the result back, and loops until it produces an answer (bounded by maxSteps, default 5). Works on every backend.

import { generateText } from 'expo-ai-kit';

const { text } = await generateText(
  [{ role: 'user', content: 'What should I wear in Paris today?' }],
  {
    tools: {
      getWeather: {
        description: 'Get the current weather for a city.',
        parameters: {
          type: 'object',
          properties: { city: { type: 'string' } },
          required: ['city'],
        },
        execute: async ({ city }: { city: string }) => fetchWeather(city),
      },
    },
  },
);

Omit a tool's execute to gate it yourself: the loop stops with finishReason: 'tool-calls' and hands you the proposed call to confirm before running. Keep tool sets small and parameter schemas flat — on-device models pick tools more reliably that way.

Embeddings & RAG

Turn text into vectors for semantic search and retrieval-augmented generation — find the chunks of your own documents most relevant to a question, then feed them to the model. All on-device: your data never leaves the phone.

import { embed, chunkText, createVectorStore, sendMessage } from 'expo-ai-kit';

// 1. Index your document once: split → embed → store
const chunks = chunkText(document);                 // overlapping, sentence-aware chunks
const { embeddings } = await embed(chunks);          // one vector per chunk
const store = createVectorStore<{ text: string }>();
store.addMany(chunks.map((text, i) => ({ id: `c${i}`, vector: embeddings[i], metadata: { text } })));

// 2. At query time: embed the question, retrieve the top matches, answer from them
const { embeddings: [q] } = await embed([question]);
const context = store.search(q, { topK: 4 }).map((h) => h.metadata!.text).join('\n\n');

const { text } = await sendMessage([
  { role: 'system', content: `Answer using only this context:\n${context}` },
  { role: 'user', content: question },
]);

embed() is backed by Apple's NLContextualEmbedding — a zero-download, OS-maintained model (iOS 17+, works even without Apple Intelligence). iOS-only for now; on Android it throws DEVICE_NOT_SUPPORTED (MediaPipe support is planned). The toolkit — chunkText, cosineSimilarity, and the createVectorStore (add / search top-k / toJSON for persistence) — is pure JS and works on both platforms with any vector source.

Vercel AI SDK

expo-ai-kit ships a first-class AI SDK provider: point model: at expoAiKit() and the AI SDK's generateText / streamText / generateObject / embed run on-device — reuse code, examples, and patterns written for cloud providers, with no API key and no data leaving the phone.

npm i ai   # AI SDK 6+ (the provider implements LanguageModelV3; AI SDK 7 accepts it too)
import { generateText, streamText, embed } from 'ai';
import { expoAiKit } from 'expo-ai-kit/ai';

// Text — the active on-device model (OS built-in by default)
const { text } = await generateText({
  model: expoAiKit(),
  prompt: 'Capital of France?',
});

// Streaming — pass a specific model id to activate it first
const result = streamText({
  model: expoAiKit('gemma-e2b'),  // must be downloaded; settings ride along:
  // model: expoAiKit('gemma-e2b', { generation: { temperature: 0.7 } }),
  prompt: 'Write a short story',
});
for await (const chunk of result.textStream) setText((t) => t + chunk);

// Embeddings (iOS) — the same NLContextualEmbedding behind embed()
const { embedding } = await embed({
  model: expoAiKit.embeddingModel(),
  value: 'sunny day at the beach',
});

Tool calling and generateObject work too — they ride the exact same prompt protocol as the core generateText/generateObject, so behavior is identical either way.

On-device caveats (the honest fine print):

  • One generation at a time. Concurrent calls reject with INFERENCE_BUSY — on-device models share a single KV-cache. Await one call before starting the next.
  • Sampling is fixed at model activation, not per call. A per-call temperature/topK is reported as an AI SDK warning and ignored; set it via expoAiKit(id, { generation }) or setModel().
  • Streaming buffers when tools or JSON output are requested — the tool-call envelope has to be parsed whole, not leaked as text deltas. Plain-text streaming is token-by-token.
  • No token usage numbers (on-device runtimes don't report them) and no image/file inputs (vision is on the roadmap). embeddingModel() is iOS-only, like embed().
  • React Native needs the AI SDK's usual polyfills (e.g. web-streams-polyfill, structuredClone, TextEncoder) — see the docs for a copy-paste setup.

The provider is a thin wrapper over the same core functions below — mix and match freely. The core stays zero-dependency; @ai-sdk/provider is an optional peer used only for types when you import expo-ai-kit/ai.

Downloadable models

Beyond the OS built-ins, you can download open models (via LiteRT-LM) and switch to them at runtime — a size ladder from sub-GB to ~4 GB:

| id | params | download | license | |---|---|---|---| | qwen3-0.6b | 0.6B | ~0.5 GB | Apache-2.0 | | gemma-e2b | 2.3B | ~2.6 GB | Gemma | | qwen3-1.7b | 1.7B | ~2.1 GB | Apache-2.0 | | qwen3-4b | 4B | ~2.7 GB | Apache-2.0 | | gemma-e4b | 4.5B | ~3.7 GB | Gemma | | phi-4-mini | 3.8B | ~3.9 GB | MIT |

import { getDownloadableModels, getRecommendedModel, downloadModel, setModel } from 'expo-ai-kit';

await getDownloadableModels(); // full catalog + per-device status, size, and license

const best = await getRecommendedModel();          // biggest model the device can run, else null
if (best) {
  await downloadModel(best.id, { onProgress: (p) => console.log(p) });
  await setModel(best.id, { generation: { temperature: 0.7 } });
  // sendMessage / streamMessage / generateObject / generateText now use it; unloadModel() reverts to the OS model
}

Each entry carries a license — check it before shipping a model to your users.

Bring your own model

Not just the built-in list — register any LiteRT-LM model at runtime with registerModel(). The download is still integrity-checked against the sha256 you provide. Use fetchModelMetadata() once at dev time to grab the hash + size from HuggingFace, then pin them in your code so the integrity guarantee is real and not just corruption-detection.

import { registerModel, fetchModelMetadata, downloadModel, setModel } from 'expo-ai-kit';

const url = 'https://huggingface.co/litert-community/Qwen3-4B/resolve/main/qwen3_4b_mixed_int4.litertlm';

// Dev time: log these once, then hardcode the returned sha256 below.
// const { sha256, sizeBytes } = await fetchModelMetadata(url);

registerModel({
  id: 'qwen3-4b-custom',
  name: 'Qwen3 4B',
  parameterCount: '4B',
  quantization: 'int4',
  downloadUrl: url,
  sha256: 'f0794bc77efeaaf4f7af815f04c483b19b8f2ae4a102cef1b7b760a25848a18e', // pinned
  sizeBytes: 2_659_057_664,
  contextWindow: 4096,
  minRamBytes: 3_000_000_000,
  supportedPlatforms: ['ios', 'android'],
  license: 'Apache-2.0',
});

await downloadModel('qwen3-4b-custom');
await setModel('qwen3-4b-custom');

Custom models are in-memory — call registerModel() at startup each launch (the downloaded file persists on disk, so its 'downloaded' status survives restarts once re-registered).

API

Inference: isAvailable, sendMessage, streamMessage, generateObject, generateText. Embeddings & RAG: embed, chunkText, cosineSimilarity, createVectorStore. AI SDK provider (expo-ai-kit/ai): expoAiKit, createExpoAiKit. Models: getBuiltInModels, getDownloadableModels, getDownloadedModels, getRecommendedModel, downloadModel, cancelDownload, deleteModel, setModel, unloadModel, getActiveModel. Custom models: registerModel, unregisterModel, getRegisteredModels, fetchModelMetadata.

Failures throw ModelError with a typed .code. Full TypeScript definitions ship with the package — see the docs for the complete reference.

License

MIT