Skip to content

Capacitor quickstart: on-device AI

What you will have

A Capacitor app that runs one text task on the device’s own model, or shows why it can’t. No API keys, no server.

Version: @independo/capacitor-inderun 1.2.0, which includes IndeRun 0.4.0. The examples on this page are type-checked against it (2026-11-10).

Before you start

Install

npm install @independo/capacitor-inderun@1.2.0 @capacitor/core@8

With pnpm, use pnpm add with the same two packages. Don’t sync yet: set the iOS and Android values below first.

iOS. Set the deployment target to 16.0 in your Xcode project before you run npx cap sync ios. The Capacitor CLI reads that value to generate CapApp-SPM/Package.swift; syncing with the default leaves a manifest below the plugin’s floor, and the build fails with:

requires minimum platform version 16.0 for the iOS platform, but this target supports 15.0

Android. Three settings in your app, none of which npx cap add android sets for you:

Setting Value Failure if unset
compileSdkVersion in android/variables.gradle 37 requires libraries and applications that depend on it to compile against version 37 or later
Android Gradle plugin 9.1.0 or newer (Gradle 9.x) requires Android Gradle plugin 9.1.0 or higher
org.jetbrains.kotlin:kotlin-gradle-plugin:2.4.10 on the app’s buildscript classpath n/a Module was compiled with an incompatible version of Kotlin … metadata is 2.4.0, expected version is 2.2.0

The plugin README has the full list, including minSdkVersion 26 and the buildscript snippet for the Kotlin plugin: Host Project Requirements (opens in new tab) (pinned to v1.2.0). Failures are also listed in Troubleshooting.

Then sync the native projects:

npx cap sync

Check the device

Ask what the device can run before you run anything:

import { createIndeRunCapacitor } from "@independo/capacitor-inderun";
const ai = createIndeRunCapacitor();
for (const provider of await ai.checkCapabilities()) {
const { available, reason } = provider.capabilities;
console.log(
provider.providerId,
provider.descriptor.type, // "local" or "cloud"
available ? "available" : `unavailable: ${reason}`
);
}

checkCapabilities() returns one entry per registered provider. On iOS the on-device provider is Apple Foundation Models (apple_foundation_models); on Android it is ML Kit GenAI (android_mlkit_genai). On an eligible device the log line for that provider reads like this:

apple_foundation_models local available

On a device without an eligible model, available is false and reason carries the platform’s explanation:

apple_foundation_models local unavailable: <reason reported by the platform>

The exact reason text comes from the platform. Typical causes are a device that isn’t eligible, Apple Intelligence turned off, a model that isn’t ready yet (iOS), or Gemini Nano not supported or not yet downloaded (Android). This output has not been verified on a device for this page.

Run local-only

import { createIndeRunCapacitor } from "@independo/capacitor-inderun";
// iOS and Android register the platform's on-device model automatically.
// No cloud provider is configured, so no request can be routed to the cloud.
const ai = createIndeRunCapacitor();
try {
const result = await ai.run({
schemaVersion: "1.0",
task: { kind: "text_to_text" },
prompt: "Rewrite in plain language: Your appointment has been rescheduled.",
constraints: { privacy: "local_required" }
});
console.log(result.output.text, result.provenance?.providerId);
} catch (error) {
// No eligible on-device model (unsupported device, Apple Intelligence off,
// Gemini Nano not downloaded, …): refused before execution, with reasons.
console.warn(error);
}

No cloud provider is configured, so local_required can only succeed on the device. If no on-device model is eligible, the request is refused before anything runs.

Read the outcome

import { createIndeRunCapacitor, type IndeRunError } from "@independo/capacitor-inderun";
const ai = createIndeRunCapacitor();
try {
const result = await ai.run({
schemaVersion: "1.0",
task: { kind: "text_to_text" },
prompt: "Rewrite in plain language: Your appointment has been rescheduled.",
constraints: { privacy: "local_required" }
});
console.log(result.output.text);
console.log(result.provenance?.providerId, result.provenance?.locality); // locality: "on_device"
} catch (error) {
const refusal = error as IndeRunError;
if (refusal.errorClass === "CapabilityMismatch") {
// Refused before anything ran. `details.rejectedProviders` lists each
// registered provider and the reason codes it was skipped for.
console.warn(refusal.message, refusal.details?.rejectedProviders);
} else {
throw error;
}
}

Success. output.text is the result. provenance.providerId names the provider that ran the task and provenance.locality is "on_device".

Refusal. run() rejects with an IndeRunError whose errorClass is "CapabilityMismatch" and whose message reads like No eligible local provider found for task 'text_to_text'. The error’s details.rejectedProviders lists each registered provider with the reason codes it was skipped for, for example capability_unavailable or privacy_constraint. The plugin forwards details from the native side in its source; treat the exact shape on your device as something to confirm in your own run.

A refusal is correct behaviour, not a setup error: the device cannot run the task under your rule, and IndeRun refused before any provider ran. (provenance.routeRejections exists only on a successful result, where it lists providers skipped on the way.)

Next