From 5a7364bb62bbf8bc628c9ac7ed02f17da5f67202 Mon Sep 17 00:00:00 2001 From: Jaime Barreto Date: Mon, 27 Jul 2026 20:55:19 +0100 Subject: [PATCH] feat: expose apple intelligence availability reason isAvailable() collapsed SystemLanguageModel.Availability into a boolean, so apps could not tell an ineligible device apart from Apple Intelligence simply being switched off, and had no way to point the user at Settings. Add getAvailability() alongside it, returning the reason Apple already exposes via .unavailable(reason). MODEL_UNAVAILABLE errors now carry the same detail instead of a single generic message. isAvailable() keeps its boolean contract and is now derived from getAvailability(). --- .../apple-llm/ios/AppleAvailability.swift | 36 +++++++++++++++ packages/apple-llm/ios/AppleLLM.mm | 4 ++ packages/apple-llm/ios/AppleLLMError.swift | 6 +-- packages/apple-llm/ios/AppleLLMImpl.swift | 45 ++++++++++++++----- .../apple-llm/src/AppleFoundationModels.ts | 1 + packages/apple-llm/src/NativeAppleLLM.ts | 22 +++++++++ packages/apple-llm/src/ai-sdk.ts | 1 + packages/apple-llm/src/index.ts | 1 + .../references/apple-provider.md | 6 +++ website/src/docs/apple/generating.md | 44 ++++++++++++++++++ .../src/docs/apple/running-on-simulator.md | 1 + 11 files changed, 153 insertions(+), 14 deletions(-) create mode 100644 packages/apple-llm/ios/AppleAvailability.swift diff --git a/packages/apple-llm/ios/AppleAvailability.swift b/packages/apple-llm/ios/AppleAvailability.swift new file mode 100644 index 00000000..cd242f2e --- /dev/null +++ b/packages/apple-llm/ios/AppleAvailability.swift @@ -0,0 +1,36 @@ +// +// AppleAvailability.swift +// AppleLLM +// + +import Foundation + +/// Mirrors `SystemLanguageModel.Availability` so that the reason why Apple +/// Intelligence is unavailable survives the bridge to JavaScript. +enum AppleAvailability: String { + case available + case deviceNotEligible + case appleIntelligenceNotEnabled + case modelNotReady + case unsupportedOS + case unknown + + /// Human readable explanation, used to give `MODEL_UNAVAILABLE` errors the + /// same level of detail as `getAvailability()`. + var unavailableDescription: String { + switch self { + case .available: + return "Apple Intelligence model is available" + case .deviceNotEligible: + return "Apple Intelligence model is not available: this device is not eligible for Apple Intelligence" + case .appleIntelligenceNotEnabled: + return "Apple Intelligence model is not available: Apple Intelligence is not enabled in Settings" + case .modelNotReady: + return "Apple Intelligence model is not available: the model is not ready yet, it may still be downloading" + case .unsupportedOS: + return "Apple Intelligence not available on this iOS version" + case .unknown: + return "Apple Intelligence model is not available" + } + } +} diff --git a/packages/apple-llm/ios/AppleLLM.mm b/packages/apple-llm/ios/AppleLLM.mm index 1b5d70d9..673d0b24 100644 --- a/packages/apple-llm/ios/AppleLLM.mm +++ b/packages/apple-llm/ios/AppleLLM.mm @@ -174,5 +174,9 @@ - (nonnull NSNumber *)isAvailable { return @([_llm isAvailable]); } +- (nonnull NSString *)getAvailability { + return [_llm getAvailability]; +} + @end diff --git a/packages/apple-llm/ios/AppleLLMError.swift b/packages/apple-llm/ios/AppleLLMError.swift index 8b9a7a36..66e9e1f0 100644 --- a/packages/apple-llm/ios/AppleLLMError.swift +++ b/packages/apple-llm/ios/AppleLLMError.swift @@ -8,7 +8,7 @@ import Foundation enum AppleLLMError: Error, LocalizedError { - case modelUnavailable + case modelUnavailable(AppleAvailability) case unsupportedOS case generationError(String) case streamNotFound(String) @@ -21,8 +21,8 @@ enum AppleLLMError: Error, LocalizedError { var errorDescription: String? { switch self { - case .modelUnavailable: - return "Apple Intelligence model is not available" + case .modelUnavailable(let availability): + return availability.unavailableDescription case .unsupportedOS: return "Apple Intelligence not available on this iOS version" case .generationError(let message): diff --git a/packages/apple-llm/ios/AppleLLMImpl.swift b/packages/apple-llm/ios/AppleLLMImpl.swift index b1f0aed7..009d2dd2 100644 --- a/packages/apple-llm/ios/AppleLLMImpl.swift +++ b/packages/apple-llm/ios/AppleLLMImpl.swift @@ -20,19 +20,39 @@ public class AppleLLMImpl: NSObject { private var streamTasks: [String: Task] = [:] - @objc - public func isAvailable() -> Bool { + private func currentAvailability() -> AppleAvailability { #if canImport(FoundationModels) if #available(iOS 26, *) { - return SystemLanguageModel.default.availability == .available + switch SystemLanguageModel.default.availability { + case .available: + return .available + case .unavailable(.deviceNotEligible): + return .deviceNotEligible + case .unavailable(.appleIntelligenceNotEnabled): + return .appleIntelligenceNotEnabled + case .unavailable(.modelNotReady): + return .modelNotReady + case .unavailable: + return .unknown + } } else { - return false + return .unsupportedOS } #else - return false + return .unsupportedOS #endif } + @objc + public func getAvailability() -> String { + return currentAvailability().rawValue + } + + @objc + public func isAvailable() -> Bool { + return currentAvailability() == .available + } + @objc public func countTokens( _ text: String, @@ -41,10 +61,11 @@ public class AppleLLMImpl: NSObject { ) { #if canImport(FoundationModels) if #available(iOS 26.4, *) { - guard SystemLanguageModel.default.availability == .available else { + let availability = currentAvailability() + guard availability == .available else { reject( "MODEL_UNAVAILABLE", - "Apple Intelligence model is not available", + availability.unavailableDescription, nil ) return @@ -77,8 +98,9 @@ public class AppleLLMImpl: NSObject { ) { #if canImport(FoundationModels) if #available(iOS 26, *) { - guard SystemLanguageModel.default.availability == .available else { - rejectWithAppleError(.modelUnavailable, reject: reject) + let availability = currentAvailability() + guard availability == .available else { + rejectWithAppleError(.modelUnavailable(availability), reject: reject) return } @@ -144,8 +166,9 @@ public class AppleLLMImpl: NSObject { ) { #if canImport(FoundationModels) if #available(iOS 26, *) { - guard SystemLanguageModel.default.availability == .available else { - emitStreamError(.modelUnavailable, streamId: streamId, onError: onError) + let availability = currentAvailability() + guard availability == .available else { + emitStreamError(.modelUnavailable(availability), streamId: streamId, onError: onError) return } diff --git a/packages/apple-llm/src/AppleFoundationModels.ts b/packages/apple-llm/src/AppleFoundationModels.ts index 2cae01dc..c7978fb0 100644 --- a/packages/apple-llm/src/AppleFoundationModels.ts +++ b/packages/apple-llm/src/AppleFoundationModels.ts @@ -13,6 +13,7 @@ const nativeAppleLLM = NativeAppleLLM as Spec & { const AppleFoundationModels: Spec = { isAvailable: () => NativeAppleLLM.isAvailable(), + getAvailability: () => NativeAppleLLM.getAvailability(), countTokens: (text) => { if (typeof nativeAppleLLM.countTokens !== 'function') { return Promise.reject(new Error(tokenCountingUnavailableMessage)) diff --git a/packages/apple-llm/src/NativeAppleLLM.ts b/packages/apple-llm/src/NativeAppleLLM.ts index 88a7456e..17930a50 100644 --- a/packages/apple-llm/src/NativeAppleLLM.ts +++ b/packages/apple-llm/src/NativeAppleLLM.ts @@ -10,6 +10,27 @@ export interface AppleMessage { content: string } +/** + * Availability of Apple Intelligence, mirroring `SystemLanguageModel.Availability`. + * + * - `available` - the model is ready to use + * - `deviceNotEligible` - the hardware does not support Apple Intelligence + * - `appleIntelligenceNotEnabled` - the user has not turned Apple Intelligence + * on, so they can be pointed at Settings + * - `modelNotReady` - Apple Intelligence is on but the model is not downloaded + * yet, so it is worth retrying later + * - `unsupportedOS` - the device runs an OS older than iOS 26 + * - `unknown` - Apple reported a reason this version of the library does not + * know about yet + */ +export type AppleAvailability = + | 'available' + | 'deviceNotEligible' + | 'appleIntelligenceNotEnabled' + | 'modelNotReady' + | 'unsupportedOS' + | 'unknown' + export interface AppleGenerationOptions { temperature?: number maxTokens?: number @@ -36,6 +57,7 @@ export type StreamErrorEvent = { export interface Spec extends TurboModule { isAvailable(): boolean + getAvailability(): AppleAvailability countTokens(text: string): Promise generateText( messages: AppleMessage[], diff --git a/packages/apple-llm/src/ai-sdk.ts b/packages/apple-llm/src/ai-sdk.ts index d47e6539..1fcf5d6e 100644 --- a/packages/apple-llm/src/ai-sdk.ts +++ b/packages/apple-llm/src/ai-sdk.ts @@ -43,6 +43,7 @@ export function createAppleProvider({ return createLanguageModel() } provider.isAvailable = () => NativeAppleLLM.isAvailable() + provider.getAvailability = () => NativeAppleLLM.getAvailability() provider.languageModel = createLanguageModel provider.textEmbeddingModel = (options: AppleEmbeddingOptions = {}) => { return new AppleTextEmbeddingModel(options) diff --git a/packages/apple-llm/src/index.ts b/packages/apple-llm/src/index.ts index 0e73c5e5..b42b59a2 100644 --- a/packages/apple-llm/src/index.ts +++ b/packages/apple-llm/src/index.ts @@ -3,6 +3,7 @@ export { default as AppleFoundationModels } from './AppleFoundationModels' export type { AppleLLMError, AppleLLMErrorCode } from './errors' export { AppleLLMErrorCodes } from './errors' export { default as AppleEmbeddings } from './NativeAppleEmbeddings' +export type { AppleAvailability } from './NativeAppleLLM' export { default as AppleSpeech, VoiceInfo } from './NativeAppleSpeech' export { default as AppleTranscription } from './NativeAppleTranscription' export { default as AppleUtils } from './NativeAppleUtils' diff --git a/skills/react-native-ai/references/apple-provider.md b/skills/react-native-ai/references/apple-provider.md index dbf96554..8f2a6ef8 100644 --- a/skills/react-native-ai/references/apple-provider.md +++ b/skills/react-native-ai/references/apple-provider.md @@ -47,6 +47,12 @@ if (apple.isAvailable()) { } ``` +Use `apple.getAvailability()` when you need to know *why* it is unavailable: +`'available' | 'deviceNotEligible' | 'appleIntelligenceNotEnabled' | +'modelNotReady' | 'unsupportedOS' | 'unknown'`. In particular +`'appleIntelligenceNotEnabled'` is user-fixable, so prompt them to turn Apple +Intelligence on in Settings instead of showing a generic error. + ### 3. Model Types | Type | Method | Use Case | Documentation | diff --git a/website/src/docs/apple/generating.md b/website/src/docs/apple/generating.md index 07fda3c6..e6a1c34b 100644 --- a/website/src/docs/apple/generating.md +++ b/website/src/docs/apple/generating.md @@ -203,6 +203,47 @@ if (!apple.isAvailable()) { } ``` +### Availability Status + +`isAvailable()` only tells you whether you can generate right now. When it +returns `false`, use `getAvailability()` to find out why, so you can show the +user something more useful than a generic error: + +```typescript +import { apple, type AppleAvailability } from '@react-native-ai/apple'; + +function describeAvailability(status: AppleAvailability) { + switch (status) { + case 'available': + return null; + case 'appleIntelligenceNotEnabled': + // Actionable: the user can fix this in Settings + return 'Turn on Apple Intelligence in Settings to use this feature.'; + case 'modelNotReady': + // Temporary: the model is still downloading, worth retrying later + return 'Apple Intelligence is still getting ready. Please try again later.'; + case 'deviceNotEligible': + case 'unsupportedOS': + case 'unknown': + // Not recoverable on this device: fall back to another provider + return null; + } +} + +const message = describeAvailability(apple.getAvailability()); +``` + +| Status | Meaning | +| ----------------------------- | ------------------------------------------------------------- | +| `available` | The model is ready to use | +| `deviceNotEligible` | The hardware does not support Apple Intelligence | +| `appleIntelligenceNotEnabled` | The user has not turned Apple Intelligence on in Settings | +| `modelNotReady` | Apple Intelligence is on, but the model is not downloaded yet | +| `unsupportedOS` | The device runs an OS older than iOS 26 | +| `unknown` | Apple reported a reason this library does not know about yet | + +`isAvailable()` is equivalent to `getAvailability() === 'available'`. + ## Context Window Apple Foundation Models have a fixed context window of 4096 tokens. This limit applies to the full request context, including system instructions, previous conversation messages, tool definitions, schemas, and the current user prompt. @@ -454,6 +495,9 @@ import { AppleFoundationModels } from '@react-native-ai/apple' // Check if Apple Intelligence is available const isAvailable = AppleFoundationModels.isAvailable() +// Or check why it is unavailable +const availability = AppleFoundationModels.getAvailability() + // Generate text responses const messages = [{ role: 'user', content: 'Hello' }] const options = { temperature: 0.7, maxTokens: 100 } diff --git a/website/src/docs/apple/running-on-simulator.md b/website/src/docs/apple/running-on-simulator.md index cf5b6b2e..c886e566 100644 --- a/website/src/docs/apple/running-on-simulator.md +++ b/website/src/docs/apple/running-on-simulator.md @@ -67,6 +67,7 @@ If Apple Intelligence is enabled but models aren't working: - Ensure the model download completed successfully on macOS - Restart the iOS Simulator - Verify that `apple.isAvailable()` returns `true` in your code (see [Availability Check](./generating#availability-check)) +- If it returns `false`, call `apple.getAvailability()` to see the exact reason (see [Availability Status](./generating#availability-status)) - Check that your app is running on iOS 26+ in the simulator ## API Availability