API Reference

The complete public API of expo-ai-kit. Everything runs on-device, on both iOS and Android.

iOSAndroid

Messages everywhere use the same shape: { role: 'system' | 'user' | 'assistant'; content: string }. On-device models are stateless — pass the full conversation history on every call.

isAvailable()

Check whether the current device supports its built-in on-device model. Returns false on unsupported platforms and devices. On Android, true means ML Kit is supported even when its OS-managed model still needs to download.

function isAvailable(): Promise<boolean>
import { isAvailable } from 'expo-ai-kit';

if (await isAvailable()) {
  await prepareBuiltInModel();
}

Availability is not preparation

Call isAvailable() to decide whether to show the feature. Await prepareBuiltInModel() before inference so Android can finish its first-use model download.

prepareBuiltInModel()

Make the platform's built-in generation model ready. Android downloads the AICore-managed ML Kit model when needed. iOS validates Apple Foundation Models availability. Repeated calls are safe and resolve immediately when the model is already ready.

function prepareBuiltInModel(): Promise<void>
import { isAvailable, prepareBuiltInModel } from 'expo-ai-kit';

if (!(await isAvailable())) {
  throw new Error('On-device AI is not supported on this device');
}

await prepareBuiltInModel();

Throws DEVICE_NOT_SUPPORTED when the built-in model cannot run and DOWNLOAD_FAILED if Android cannot prepare its model.

sendMessage()

Send a conversation and get a single response.

function sendMessage(
  messages: LLMMessage[],
  options?: LLMSendOptions,
): Promise<LLMResponse>

Options: systemPrompt?: string (used only if the array has no system message), signal?: AbortSignal.

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

const { text } = await sendMessage(
  [{ role: 'user', content: 'Capital of France?' }],
  { systemPrompt: 'Answer in one word.' },
);

Cancellation

On-device, non-streaming generation can't always be interrupted mid-decode — signal always unblocks the caller, but the model may keep running in the background (a new call throws INFERENCE_BUSY until it finishes). To truly interrupt, use streamMessage().stop().

streamMessage()

Stream a response token-by-token. Returns a handle with a promise that resolves with the final text and a stop() to cancel.

function streamMessage(
  messages: LLMMessage[],
  onToken: (event: LLMStreamEvent) => void,
  options?: LLMStreamOptions,
): LLMStreamHandle // { promise, stop }
import { streamMessage } from 'expo-ai-kit';

const { promise, stop } = streamMessage(
  [{ role: 'user', content: 'Write a short story' }],
  (event) => {
    setText(event.accumulatedText); // event.token, event.isDone also available
  },
);

await promise; // resolves with { text }; call stop() to cancel early

generateObject()

Get a typed object validated against a JSON Schema. See the Structured Output guide for the full story.

function generateObject<T = unknown>(
  messages: LLMMessage[],
  schema: JSONSchema,
  options?: GenerateObjectOptions,
): Promise<GenerateObjectResult<T>> // { object, text }

Options: systemPrompt?, signal?, maxRepairAttempts? (default 2). Throws INFERENCE_FAILED if no schema-valid JSON is produced after the repair attempts.

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

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

generateText()

Generate text, optionally letting the model call tools you provide. See the Tool Calling guide.

function generateText(
  messages: LLMMessage[],
  options?: GenerateTextOptions,
): Promise<GenerateTextResult>
// { text, steps, toolCalls, toolResults, finishReason }

Options: tools?: ToolSet, maxSteps? (default 5), systemPrompt?, signal?, maxRepairAttempts? (default 2).

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

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

embed()

iOSAndroidNew

Turn text into embedding vectors for semantic search / RAG. See the Embeddings & RAG guide. iOS (17+): Apple's zero-download NLContextualEmbedding language selects the script model. Android: EmbeddingGemma 300M via MediaPipe TextEmbedder — opt-in via the androidEmbeddings config-plugin flag, model prepared with prepareEmbeddingModel(); embed() itself never downloads.

function embed(
  texts: string[],
  options?: {
    task?: 'semantic-similarity' | 'retrieval-query' | 'retrieval-document';
    language?: string; // BCP-47, default 'en' — selects the iOS script model; ignored on Android
  },
): Promise<EmbedResult>
// { embeddings: number[][]; dimensions: number; model: { id: string; revision: string } }
import { embed } from 'expo-ai-kit';

const { embeddings, dimensions, model } = await embed(
  ['hello world', 'goodbye'],
  { task: 'retrieval-document' },
);
embeddings.length; // 2 — one vector per input, in order
model;             // identity — indexes are only comparable under identical identity

Embedding model lifecycle

iOSAndroidNew

Readiness and asset management for the embedding model. prepareEmbeddingModel() is the only call that downloads — on Android it fetches the ~184 MB EmbeddingGemma bundle (SHA-256-verified, atomic install, fails closed on partial/corrupt downloads); on iOS it prefetches the OS-managed assets for a language. Cancel/delete are Android-side (safe no-ops on iOS).

function getEmbeddingModelStatus(options?: { language?: string }): Promise<{
  status: 'not-downloaded' | 'downloading' | 'downloaded';
  sizeBytes: number; // ~184 MB pinned on Android; 0 on iOS (OS-managed)
  model: { id: string; revision: string };
}>
function prepareEmbeddingModel(options?: {
  language?: string;
  onProgress?: (progress: number) => void; // 0–1 (Android)
}): Promise<void>
function cancelEmbeddingModelDownload(): Promise<void>
function deleteEmbeddingModel(): Promise<void>
function getSupportedEmbeddingLanguages(): Promise<string[]> // iOS catalog; [] on Android
function stripThinking(text: string): { text: string; reasoning: string }
// pure helper — splits <think>…</think> reasoning (Qwen3-style models) from an answer

Speech-to-Text

iOSAndroidNew

On-device transcription — live microphone streaming and audio-file transcripts. Opt-in via the config plugin's speech flag; runs its own single-flight (SPEECH_BUSY), independent of text generation. See the Speech-to-Text guide for platform behavior and examples.

function transcribe(options: {
  audio: { uri: string } | { base64: string; mediaType?: string };
  locale?: string;        // BCP-47; defaults to the device locale
  signal?: AbortSignal;
}): Promise<{
  text: string;
  segments: { text: string; startSeconds: number; endSeconds: number }[]; // iOS; [] on Android
  language?: string;
  durationSeconds?: number;
}>

function streamTranscription(
  onUpdate: (update: { text: string; isFinal: boolean }) => void,
  options?: { locale?: string }
): { promise: Promise<TranscribeResult>; stop: () => void }

function getSpeechRecognitionAvailability(options?: { locale?: string }): Promise<
  | { status: 'available' }
  | { status: 'downloadable' | 'downloading' }
  | { status: 'unavailable';
      reason: 'platform' | 'os-version' | 'device' | 'locale' | 'not-enabled' }
>
function prepareSpeechRecognition(options?: {
  locale?: string;
  onProgress?: (progress: number) => void; // 0–1
}): Promise<void>
function getSupportedSpeechLocales(): Promise<string[]>
function getSpeechPermissionsAsync(): Promise<SpeechPermissionResponse>
function requestSpeechPermissionsAsync(): Promise<SpeechPermissionResponse>

AI SDK Provider

iOSAndroidNew

A Vercel AI SDK provider (LanguageModelV3, AI SDK 6+) over the on-device engine, exported from the expo-ai-kit/ai subpath. See the Vercel AI SDK guide for setup (polyfills), examples, and the on-device caveats.

import { expoAiKit, createExpoAiKit } from 'expo-ai-kit/ai';

// LanguageModelV3 — pass to generateText / streamText / generateObject
expoAiKit(modelId?: string, settings?: ExpoAiKitModelSettings): LanguageModelV3
//   modelId: 'auto' (default — the active model) or any setModel() id
//   settings: same shape as setModel() options; applied on activation

// EmbeddingModelV3 over embed() — resolves the platform default
// ('apple-nl-contextual' on iOS, 'embedding-gemma-300m' on Android)
expoAiKit.embeddingModel(modelId?: string, settings?: { task?: EmbeddingTask; language?: string }): EmbeddingModelV3

// Factory (a fresh provider instance)
createExpoAiKit(): ExpoAiKitProvider
import { generateText } from 'ai';
import { expoAiKit } from 'expo-ai-kit/ai';

const { text } = await generateText({
  model: expoAiKit(),
  prompt: 'Capital of France?',
});

RAG Toolkit

iOSAndroid

Pure-JS helpers for retrieval. They work on every platform with any source of vectors, since they only deal in plain number[].

chunkText()

Split a document into overlapping, sentence-aware chunks sized for embedding.

function chunkText(
  text: string,
  options?: { chunkSize?: number; overlap?: number }, // defaults: 1000, min(200, chunkSize / 5)
): string[]

cosineSimilarity()

Magnitude-invariant relevance score in [-1, 1]. Throws on a length mismatch.

function cosineSimilarity(a: number[], b: number[]): number

createVectorStore()

A lightweight in-memory vector store. Add records, then search() by a query vector for the top-k most similar. Snapshot with toJSON() and rehydrate by passing it back in.

function createVectorStore<M = unknown>(
  initial?: VectorRecord<M>[],
): VectorStore<M>

// VectorStore<M>:
//   add(id, vector, metadata?) · addMany(records) · get(id) · remove(id)
//   clear() · size · toJSON()
//   search(query, { topK = 10, minScore? }): VectorSearchResult<M>[]
import { createVectorStore } from 'expo-ai-kit';

const store = createVectorStore<{ text: string }>();
store.addMany(chunks.map((text, i) => ({ id: `c${i}`, vector: embeddings[i], metadata: { text } })));

const hits = store.search(queryVector, { topK: 4 });
// → [{ id, vector, metadata, score }, …] sorted by score, highest first

Model Management

Switch between the OS built-in models and downloadable ones. See the Models guide for a walkthrough.

getBuiltInModels()

List the OS built-in models (Apple FM on iOS, ML Kit on Android).

function getBuiltInModels(): Promise<BuiltInModel[]>

getDownloadableModels()

The full downloadable catalog (built-in registry + any registerModel() entries), enriched with per-device status, size, license, and meetsRequirements.

function getDownloadableModels(): Promise<DownloadableModel[]>

getDownloadedModels()

Return only downloadable models already present on the device, including models currently loading or ready for inference.

function getDownloadedModels(): Promise<DownloadableModel[]>

getRecommendedModel()

The most capable model the current device can actually run, or null.

function getRecommendedModel(): Promise<DownloadableModel | null>

downloadModel()

Download a model with integrity verification (SHA256). Reports progress 0–1.

function downloadModel(
  modelId: string,
  options?: { onProgress?: (progress: number) => void },
): Promise<void>

cancelDownload()

Cancel an in-flight download; the downloadModel promise rejects with DOWNLOAD_CANCELLED.

function cancelDownload(modelId: string): Promise<void>

deleteModel()

Delete a downloaded model file from disk (unloads it first if active).

function deleteModel(modelId: string): Promise<void>

setModel()

Activate a model for inference — the sole gatekeeper of model validity. For downloadable models this loads weights into memory; only one is loaded at a time.

function setModel(modelId: string, options?: SetModelOptions): Promise<void>
// SetModelOptions: { backend?: 'auto' | 'gpu' | 'cpu'; generation?: GenerationConfig }
await setModel('qwen3-1.7b', { generation: { temperature: 0.7, topK: 40 } });

unloadModel()

Unload the current downloadable model and revert to the OS built-in.

function unloadModel(): Promise<void>

getActiveModel()

The id of the currently active model (e.g. 'apple-fm').

function getActiveModel(): string

Custom Models

Register any LiteRT-LM model at runtime. See Bring Your Own Model.

registerModel()

Add a custom downloadable model. Validates the entry and rejects ids that collide with a built-in (curated or native) model.

function registerModel(entry: ModelRegistryEntry): void

unregisterModel()

Remove a custom model (returns true if one was removed). Does not delete any downloaded file.

function unregisterModel(modelId: string): boolean

getRegisteredModels()

All custom models registered this session.

function getRegisteredModels(): ModelRegistryEntry[]

fetchModelMetadata()

Look up a model file's sha256 and sizeBytes from a HuggingFace resolve URL, to fill in a registerModel() entry.

function fetchModelMetadata(
  downloadUrl: string,
): Promise<{ sha256: string; sizeBytes: number }>

Pin the hash

Run this once at dev time and hardcode the returned sha256. Fetching it at runtime only catches transit corruption, not a changed upstream repo.

Types

type LLMRole = 'system' | 'user' | 'assistant';
type LLMMessage = { role: LLMRole; content: string };
type LLMResponse = { text: string };

type LLMSendOptions = { systemPrompt?: string; signal?: AbortSignal };
type LLMStreamOptions = { systemPrompt?: string };
type LLMStreamHandle = { promise: Promise<LLMResponse>; stop: () => void };
type LLMStreamEvent = {
  sessionId: string; token: string; accumulatedText: string; isDone: boolean;
};

// Sampling — applied at setModel(), best-effort per backend
type InferenceBackend = 'auto' | 'gpu' | 'cpu';
type GenerationConfig = {
  temperature?: number; topK?: number; topP?: number; seed?: number; maxTokens?: number;
};
type SetModelOptions = { backend?: InferenceBackend; generation?: GenerationConfig };

// Structured output
type JSONSchema = {
  type?: JSONSchemaType | JSONSchemaType[];
  properties?: Record<string, JSONSchema>;
  required?: string[];
  items?: JSONSchema;
  enum?: ReadonlyArray<string | number | boolean | null>;
  [key: string]: unknown;
};
type GenerateObjectOptions = {
  systemPrompt?: string; signal?: AbortSignal; maxRepairAttempts?: number;
};
type GenerateObjectResult<T> = { object: T; text: string };

// Tool calling
type Tool<TArgs = any, TResult = any> = {
  description: string;
  parameters: JSONSchema;
  execute?: (args: TArgs) => TResult | Promise<TResult>;
};
type ToolSet = Record<string, Tool>;
type ToolCall = { toolName: string; args: unknown };
type ToolResult = { toolName: string; args: unknown; result: unknown };
type StepResult = { text: string; toolCalls: ToolCall[]; toolResults: ToolResult[] };
type GenerateTextFinishReason = 'stop' | 'tool-calls' | 'max-steps';
type GenerateTextOptions = {
  tools?: ToolSet; maxSteps?: number;
  systemPrompt?: string; signal?: AbortSignal; maxRepairAttempts?: number;
};
type GenerateTextResult = {
  text: string; steps: StepResult[];
  toolCalls: ToolCall[]; toolResults: ToolResult[];
  finishReason: GenerateTextFinishReason;
};

// Embeddings & RAG
type EmbeddingTask = 'semantic-similarity' | 'retrieval-query' | 'retrieval-document';
type EmbedOptions = { task?: EmbeddingTask; language?: string };
type EmbeddingModelIdentity = { id: string; revision: string };
type EmbedResult = {
  embeddings: number[][]; dimensions: number; model: EmbeddingModelIdentity;
};
type EmbeddingModelState = {
  status: 'not-downloaded' | 'downloading' | 'downloaded';
  sizeBytes: number; model: EmbeddingModelIdentity;
};
type ChunkOptions = { chunkSize?: number; overlap?: number };
type VectorRecord<M = unknown> = { id: string; vector: number[]; metadata?: M };
type VectorSearchResult<M = unknown> = VectorRecord<M> & { score: number };
type VectorSearchOptions = { topK?: number; minScore?: number };
type VectorStore<M = unknown> = {
  add(id: string, vector: number[], metadata?: M): void;
  addMany(records: VectorRecord<M>[]): void;
  get(id: string): VectorRecord<M> | undefined;
  remove(id: string): boolean;
  clear(): void;
  readonly size: number;
  search(query: number[], options?: VectorSearchOptions): VectorSearchResult<M>[];
  toJSON(): VectorRecord<M>[];
};

// Models
type BuiltInModel = {
  id: string; name: string; available: boolean;
  platform: 'ios' | 'android'; contextWindow: number;
};
type DownloadableModelStatus =
  | 'not-downloaded' | 'downloading' | 'downloaded' | 'loading' | 'ready';
type DownloadableModel = {
  id: string; name: string; parameterCount: string; license: string;
  sizeBytes: number; contextWindow: number; minRamBytes: number;
  meetsRequirements: boolean; status: DownloadableModelStatus;
};
type ModelRegistryEntry = {
  id: string; name: string; parameterCount: string; quantization: string;
  downloadUrl: string; sha256: string; sizeBytes: number;
  contextWindow: number; minRamBytes: number;
  supportedPlatforms: ('ios' | 'android')[]; license: string;
  preferredBackend?: 'auto' | 'gpu' | 'cpu'; // used when setModel gets no backend
};

Errors

Failures throw a ModelError with a typed .code and .modelId, so you can branch on the cause:

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

try {
  await setModel('gemma-e4b');
} catch (e) {
  if (e instanceof ModelError && e.code === 'MODEL_NOT_DOWNLOADED') {
    await downloadModel('gemma-e4b');
  }
}

ModelErrorCode is one of: MODEL_NOT_FOUND, MODEL_NOT_DOWNLOADED, DOWNLOAD_FAILED, DOWNLOAD_CORRUPT, DOWNLOAD_STORAGE_FULL, DOWNLOAD_CANCELLED, INFERENCE_OOM, INFERENCE_FAILED, INFERENCE_BUSY, INFERENCE_CANCELLED, MODEL_LOAD_FAILED, DEVICE_NOT_SUPPORTED, EMBEDDINGS_NOT_ENABLED, LANGUAGE_NOT_SUPPORTED, UNKNOWN.

The two embedding-specific codes: EMBEDDINGS_NOT_ENABLED — Android was built without the androidEmbeddings config-plugin flag (enabling requires a new native build); LANGUAGE_NOT_SUPPORTED — no iOS embedding model supports the requested language (the message names it; there is never a silent fall-back to the Latin model).

Single-flight inference

Only one generation runs at a time. A concurrent sendMessage / streamMessage / generateObject / generateText rejects with INFERENCE_BUSY — wait for the active one, or stop() the active stream first.