Text Generation

Chat with the model your users' phones already have, or one you download, with two functions: sendMessage() and streamMessage().

iOSAndroid

Overview

Text is the capability that needs no configuration. By default it runs on the OS model, Apple Foundation Models on iOS 26+ and the ML Kit Prompt API on supported Android devices , and you can switch to a downloadable Gemma, Qwen, or Phi model with setModel() (see Models). Every text function shares the same message shape:

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

The message APIs are stateless: pass the complete history on every call. The Multi-turn guide shows the patterns.

Availability & preparation

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

if (!(await isAvailable())) {
  // No built-in model here, offer a downloadable one (getRecommendedModel) or a fallback UI.
}

// Android may download its OS-managed model on first use; iOS validates availability.
// Resolves immediately when the model is already ready.
await prepareBuiltInModel();

Support is not readiness

On Android, isAvailable() can be true while the ML Kit model still needs its first download. Await prepareBuiltInModel() once during setup; generating before it completes throws a typed MODEL_NOT_DOWNLOADED.

Generate

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

const { text } = await sendMessage([
  { role: 'user', content: 'Explain local-first AI in one sentence.' },
]);

Stream

For a ChatGPT-style experience, stream tokens as they are produced. The handle's promise resolves with the final text and stop() ends the generation early with what was produced so far.

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

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

const { text } = await promise;

System prompts

Put a system message first in the array, or pass systemPrompt in the options, it is used only when the array has no system message.

const { text } = await sendMessage(
  [{ role: 'user', content: 'Tell me a joke' }],
  { systemPrompt: 'You are a comedian who specializes in dad jokes.' }
);

Cancellation

sendMessage() accepts an AbortSignal; aborting rejects with INFERENCE_CANCELLED. On-device, non-streaming generation cannot always be interrupted mid-decode, the caller is unblocked immediately, but the model may keep computing briefly (a new call throws INFERENCE_BUSY until it finishes). To truly interrupt a long generation, prefer streamMessage().stop().

One generation at a time

The device runs a single model context, so sendMessage, streamMessage, generateObject, and generateText share one guard: a second concurrent call rejects with INFERENCE_BUSY. Speech, vision, and embeddings have their own paths and never trip it, a voice → model → answer pipeline works as expected.

Beyond plain text

  • Structured Output:generateObject() returns a typed object validated against a JSON Schema, with a bounded repair loop.
  • Tool Calling:generateText() lets the model call functions you provide and answer from their results.
  • Embeddings & RAG: retrieve the most relevant chunks of your own data and add them to the conversation.
  • Vercel AI SDK: the same engine behind generateText / streamText from ai.