Cupertino Foundations Models
Apple Foundation Models for Flutter with on-device AI, streaming responses, multimodal prompts, speech transcription, runtime diagnostics, and an explicit path for Private Cloud Compute on iOS 27.
This package is designed as a native-first Flutter bridge. It uses Dart DTOs and Flutter platform channels on the public side, and Apple frameworks on the iOS side. It does not add runtime third-party dependencies.
Status
This package is early, iOS-only, and targets real-device experimentation with Apple Intelligence and Foundation Models.
Implemented:
- Runtime capabilities and availability diagnostics.
- Local on-device text generation through
SystemLanguageModel. LanguageModelSessionhandles for multi-turn conversation context.- One-shot
respond()convenience calls. - Streaming responses.
- Text and image prompt attachments.
- Audio file transcription through
Speech.framework. - Explicit local, automatic, and Private Cloud Compute mode selection.
- Stable Dart error codes with recovery suggestions.
- iOS example app for manual validation.
In progress:
- Native tool calling bridge.
- Native guided structured generation.
- Direct Photos picker.
- Live microphone transcription.
- Dynamic Profiles.
Platform Support
This Flutter plugin currently supports iOS only. The platforms field in pubspec.yaml is intentionally restricted to iOS so pub.dev does not advertise Android, Linux, macOS, Web, or Windows support.
| Capability | Apple API | Minimum OS | Notes |
|---|---|---|---|
| Local text generation | SystemLanguageModel, LanguageModelSession |
iOS 26+ | Requires Apple Intelligence, supported language, and downloaded model assets. |
| Conversation context | LanguageModelSession transcript |
iOS 26+ | Reuse the same FoundationModelSession; one-shot calls are stateless. |
| Streaming | streamResponse |
iOS 26+ | Emits model text updates and completion events. |
| Context size diagnostics | contextSize |
iOS 26+ | Local model context is limited; manage long conversations. |
| Image prompts | Attachment<ImageAttachmentContent> |
iOS 27+ | Requires building with Xcode 27, beta or official. Experimental in the iOS 27 SDK. |
| Reasoning options | ContextOptions.ReasoningLevel |
iOS 27+ | Requires building with Xcode 27, beta or official. Mapped from Dart ReasoningLevel. |
| Private Cloud Compute | PrivateCloudComputeLanguageModel |
iOS 27+ | Requires building with Xcode 27, beta or official, plus Apple availability, network, quota, and entitlement. |
| Audio transcription | SFSpeechURLRecognitionRequest |
iOS 13+ | Uses Speech, not Foundation Models. On-device mode requires locale support. |
Conversation Context
Yes, Apple Foundation Models support conversation context through LanguageModelSession.
Use createSession() when you want a multi-turn conversation. The native session keeps its transcript while the session is alive, so each session.respond() or session.stream() call can use previous turns as context.
final CupertinoFoundationModels models = CupertinoFoundationModels();
final FoundationModelSession session = await models.createSession(
options: const SessionOptions(
mode: ModelMode.local,
cloudPolicy: CloudPolicy.never,
instructions: 'You are a concise assistant. Answer in English.',
),
);
try {
final ModelResponse first = await session.respond(
const Prompt.text('My app helps users organize personal finances.'),
);
final ModelResponse second = await session.respond(
const Prompt.text('Based on what I just told you, suggest three feature names.'),
);
print(first.text);
print(second.text);
} finally {
await session.dispose();
}
Use models.respond() only for single-turn requests. It creates a native session, sends one prompt, then disposes the session, so it does not preserve conversation history.
Installation
Add the package to your Flutter app:
dependencies:
cupertino_fundations_models: ^0.0.2
Then import it:
import 'package:cupertino_fundations_models/cupertino_fundations_models.dart';
iOS Setup
Use a real Apple Intelligence-compatible device. Simulator support for these APIs is limited and can differ across iOS betas.
For speech transcription, add these keys to your app Info.plist:
<key>NSSpeechRecognitionUsageDescription</key>
<string>This app uses speech recognition to transcribe selected audio files.</string>
<key>NSMicrophoneUsageDescription</key>
<string>This app can use audio input for speech transcription.</string>
Features that require iOS 27 must be compiled with Xcode 27, either the beta or the official release if it is already available. For iOS 27 beta development, build with the iOS 27 SDK:
DEVELOPER_DIR=/Applications/Xcode-beta.app/Contents/Developer flutter build ios
When testing on the iOS 27 beta, Foundation Models currently work only when the device is fully configured in English, including system language and Siri language.
Availability First
Always check availability before sending a model request.
final CupertinoFoundationModels models = CupertinoFoundationModels();
final ModelAvailability availability = await models.checkAvailability(
mode: ModelMode.local,
cloudPolicy: CloudPolicy.never,
localeIdentifier: 'en_US',
);
if (!availability.isAvailable) {
throw StateError(
availability.recoverySuggestion ?? availability.reason ?? availability.status.name,
);
}
Use diagnostics when availability is not enough:
final FoundationModelsDiagnostics diagnostics = await models.getDiagnostics(
localeIdentifier: 'en_US',
);
print(diagnostics.currentLocaleIdentifier);
print(diagnostics.localAvailability.status.name);
print(diagnostics.localSupportedLanguages);
Hybrid Orchestration
Use FoundationModelsOrchestrator when your app already has a larger AI
orchestrator and wants Apple models as a primary, secondary, or cost-saving
route. The Apple bridge stays available through CupertinoFoundationModels;
the orchestrator only adds route selection and external-provider fallback.
final FoundationModelsOrchestrator ai = FoundationModelsOrchestrator(
defaults: const FoundationModelsDefaults(
localeIdentifier: 'es_CO',
instructions: 'Respond in Spanish. Prefer private, low-cost routes.',
options: GenerationOptions(maximumResponseTokens: 240),
),
router: FoundationModelsRoutingPolicy.hybrid(
allowPrivateCloud: true,
allowExternalFallback: true,
),
externalProvider: YourGeminiProvider(),
);
final String runtimeContext = await ai.buildRuntimePromptContext();
print(runtimeContext);
final OrchestratedModelResponse response = await ai.respondText(
'Resume este texto y extrae las acciones importantes.',
task: FoundationModelsTask.summarize,
);
print(response.providerName);
print(response.text);
The built-in policies are:
localOnly(): only Apple on-device models.appleFirst(): Apple local first, then optional PCC or external fallback.externalFirst(): external provider first, with optional Apple fallback.hybrid(): local-first for simple work, external-first for complex reasoning and tool-routing tasks.
External providers are app adapters. The package does not ship API clients for
Gemini, OpenAI, Anthropic, or a custom backend, but it gives those adapters the
same Prompt, GenerationOptions, StructuredSchema, and task metadata used
by Apple routes.
Local Generation
final ModelResponse response = await models.respond(
const Prompt.text('Summarize why on-device AI is useful.'),
mode: ModelMode.local,
cloudPolicy: CloudPolicy.never,
instructions: 'Answer in English with one sentence.',
options: const GenerationOptions(maximumResponseTokens: 120),
);
print(response.text);
Streaming
final FoundationModelSession session = await models.createSession(
options: const SessionOptions(
mode: ModelMode.local,
cloudPolicy: CloudPolicy.never,
),
);
try {
await for (final SessionEvent event in session.stream(
const Prompt.text('Write a short product description.'),
options: const GenerationOptions(maximumResponseTokens: 180),
)) {
switch (event) {
case TextDeltaEvent():
print(event.text);
case CompletionEvent():
print(event.response.text);
case FailureEvent():
print(event.message);
case ToolCallEvent():
print(event.name);
case UnknownSessionEvent():
print(event.payload);
}
}
} finally {
await session.dispose();
}
Treat TextDeltaEvent.text as the latest text snapshot emitted by the native stream.
Image And Text Attachments
The package can select files with the native iOS document picker and attach supported files to a prompt.
final PickedFoundationModelsFile? image = await models.pickFile(
kind: FoundationModelsFileKind.image,
);
if (image == null) {
return;
}
final ModelResponse response = await models.respond(
Prompt(
text: 'Describe the important details in this image.',
attachments: <PromptAttachment>[
image.toPromptAttachment(label: image.name),
],
),
mode: ModelMode.local,
cloudPolicy: CloudPolicy.never,
);
Current attachment behavior:
- Text files are read as UTF-8 and inserted into the prompt.
- Image files use Foundation Models image attachments on iOS 27+.
- Audio files are not attached directly to Foundation Models; transcribe them first.
Audio Transcription
Audio transcription uses Speech.framework.
final PickedFoundationModelsFile? audio = await models.pickFile(
kind: FoundationModelsFileKind.audio,
);
if (audio == null) {
return;
}
final AudioTranscriptionResult transcript = await models.transcribeAudio(
AudioTranscriptionRequest(
filePath: audio.path,
localeIdentifier: 'en_US',
mode: AudioTranscriptionMode.onDevice,
),
);
final ModelResponse response = await models.respond(
Prompt.text(transcript.text),
mode: ModelMode.local,
cloudPolicy: CloudPolicy.never,
);
Use AudioTranscriptionMode.onDevice to require local speech recognition. Use server only when your app allows Apple Speech's remote path.
Private Cloud Compute
Private Cloud Compute must be explicit.
final ModelAvailability availability = await models.checkAvailability(
mode: ModelMode.privateCloudCompute,
cloudPolicy: CloudPolicy.whenExplicit,
localeIdentifier: 'en_US',
);
if (availability.isAvailable) {
final ModelResponse response = await models.respond(
const Prompt.text('Plan a release checklist.'),
mode: ModelMode.privateCloudCompute,
cloudPolicy: CloudPolicy.whenExplicit,
);
print(response.text);
}
Do not use PCC silently. Keep CloudPolicy.never for offline behavior.
Error Handling
try {
final ModelResponse response = await models.respond(
const Prompt.text('Hello'),
mode: ModelMode.local,
cloudPolicy: CloudPolicy.never,
);
print(response.text);
} on FoundationModelsException catch (error) {
print(error.code.name);
print(error.message);
print(error.recoverySuggestion);
}
Important error codes:
appleIntelligenceDisabledassetsUnavailableunsupportedLanguageunsupportedPlatformunsupportedOsVersionprivateCloudUnavailablenetworkUnavailablequotaExceededcontextExceededspeechRecognitionDeniedspeechRecognitionUnavailable
Example App
The example app is in example/.
cd example
flutter pub get
flutter run -d <ios-device-id>
The example can inspect capabilities, diagnostics, availability, local generation, automatic mode, PCC checks, streaming, file attachments, and audio transcription.
Full advanced example source:
The example is intentionally larger than a minimal snippet because it exercises the most important runtime paths from a real iPhone: local generation, Private Cloud Compute checks, stream rendering, file attachments, and audio transcription.
Documentation
implementation_for_agents.md: integration guide for agents and maintainers.- Apple Foundation Models documentation
- Dart package publishing documentation
Agent Skill Guide
This repository includes implementation_for_agents.md, a skill-style guide for agents and maintainers. It describes the architecture, supported APIs, availability workflow, conversation context, streaming, attachments, audio transcription, validation commands, and publishing checklist.
Use it when implementing features in an app or extending the package itself.
Source Code
Source code, issues, and package metadata are linked through pubspec.yaml:
- Repository: https://github.com/sooyvilla/cupertino_fundations_models
- Issues: https://github.com/sooyvilla/cupertino_fundations_models/issues
- Documentation: https://github.com/sooyvilla/cupertino_fundations_models#readme
Package Name
The package is currently named cupertino_fundations_models. The intended English term is "foundations", but package names are permanent after publishing. Decide whether to rename before the first public release.