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