From 452cf8eb04138f7ca8700fa50cc1af90522be7d9 Mon Sep 17 00:00:00 2001 From: Mike Grabowski Date: Thu, 11 Jun 2026 21:38:26 +0200 Subject: [PATCH] feat(apple): add private cloud compute model --- README.md | 25 +- packages/apple-llm/README.md | 18 +- packages/apple-llm/ios/AppleLLM.mm | 4 + packages/apple-llm/ios/AppleLLMError.swift | 6 + packages/apple-llm/ios/AppleLLMImpl.swift | 292 +++++++++++++++--- packages/apple-llm/src/NativeAppleLLM.ts | 6 + packages/apple-llm/src/ai-sdk.ts | 53 +++- packages/apple-llm/src/errors.ts | 1 + packages/apple-llm/src/index.ts | 5 + .../references/apple-provider.md | 8 +- website/src/docs/apple/generating.md | 64 +++- website/src/docs/apple/getting-started.md | 2 +- 12 files changed, 418 insertions(+), 66 deletions(-) diff --git a/README.md b/README.md index a6236474..108755f5 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,7 @@ receiving the current app's telemetry stream. Native integration with Apple's on-device AI capabilities. **Built-in** - no model downloads required, uses system models. - **Text Generation** - Apple Foundation Models for chat and completion +- **Private Cloud Compute** - Opt-in server model for larger context and reasoning on iOS 27+ - **Embeddings** - NLContextualEmbedding for 512-dimensional semantic vectors - **Transcription** - SpeechAnalyzer for fast, accurate speech-to-text - **Speech Synthesis** - AVSpeechSynthesizer for natural text-to-speech with system voices @@ -87,6 +88,17 @@ const { text } = await generateText({ prompt: 'Explain quantum computing', }) +// Private Cloud Compute on iOS 27+ +const pcc = await generateText({ + model: apple('private-cloud-compute'), + prompt: 'Analyze this long document', + providerOptions: { + apple: { + reasoningLevel: 'moderate', + }, + }, +}) + // Generate embeddings const { embedding } = await embed({ model: apple.textEmbeddingModel(), @@ -108,12 +120,13 @@ const { audio } = await speech({ #### Availability -| Feature | iOS Version | Additional Requirements | -| ---------------- | ----------- | -------------------------- | -| Text Generation | iOS 26+ | Apple Intelligence device | -| Embeddings | iOS 17+ | - | -| Transcription | iOS 26+ | - | -| Speech Synthesis | iOS 13+ | iOS 17+ for Personal Voice | +| Feature | iOS Version | Additional Requirements | +| --------------------- | ----------- | ------------------------------------------------------------ | +| Text Generation | iOS 26+ | Apple Intelligence device | +| Private Cloud Compute | iOS 27+ | iOS 27 SDK build, Apple Intelligence device, managed entitlement | +| Embeddings | iOS 17+ | - | +| Transcription | iOS 26+ | - | +| Speech Synthesis | iOS 13+ | iOS 17+ for Personal Voice | See the [Apple documentation](https://react-native-ai.dev/docs/apple/getting-started) for detailed setup and usage guides. diff --git a/packages/apple-llm/README.md b/packages/apple-llm/README.md index 22e3c148..a7ba8ddc 100644 --- a/packages/apple-llm/README.md +++ b/packages/apple-llm/README.md @@ -5,7 +5,7 @@ A Vercel AI SDK provider for Apple Foundation Models, enabling access to Apple I **Requirements:** - iOS 26+ - Apple Intelligence enabled device -- Vercel AI SDK v5 +- Vercel AI SDK v6 - React Native New Architecture ```ts @@ -18,9 +18,25 @@ const answer = await generateText({ }) ``` +Use the Private Cloud Compute model with an iOS 27 SDK build when you need a +larger context window or reasoning: + +```ts +const answer = await generateText({ + model: apple('private-cloud-compute'), + prompt: 'Analyze this long document', + providerOptions: { + apple: { + reasoningLevel: 'moderate', + }, + }, +}) +``` + ## Features - ✅ Text generation with Apple Foundation Models +- ✅ Private Cloud Compute model selection - ✅ Structured outputs - ✅ Tool calling - ✅ Streaming diff --git a/packages/apple-llm/ios/AppleLLM.mm b/packages/apple-llm/ios/AppleLLM.mm index 1b5d70d9..c38af3fa 100644 --- a/packages/apple-llm/ios/AppleLLM.mm +++ b/packages/apple-llm/ios/AppleLLM.mm @@ -110,10 +110,12 @@ - (void)generateText:(nonnull NSArray *)messages resolve:(nonnull RCTPromiseResolveBlock)resolve reject:(nonnull RCTPromiseRejectBlock)reject { NSDictionary *opts = @{ + @"model": options.model() ?: [NSNull null], @"temperature": options.temperature().has_value() ? @(options.temperature().value()) : [NSNull null], @"maxTokens": options.maxTokens().has_value() ? @(options.maxTokens().value()) : [NSNull null], @"topP": options.topP().has_value() ? @(options.topP().value()) : [NSNull null], @"topK": options.topK().has_value() ? @(options.topK().value()) : [NSNull null], + @"reasoningLevel": options.reasoningLevel() ?: [NSNull null], @"schema": options.schema() ?: [NSNull null], @"tools": options.tools() ?: [NSNull null] }; @@ -138,10 +140,12 @@ - (void)cancelStream:(nonnull NSString *)streamId { - (void)generateStream:(nonnull NSString *)streamId messages:(nonnull NSArray *)messages options:(JS::NativeAppleLLM::AppleGenerationOptions &)options { NSDictionary *opts = @{ + @"model": options.model() ?: [NSNull null], @"temperature": options.temperature().has_value() ? @(options.temperature().value()) : [NSNull null], @"maxTokens": options.maxTokens().has_value() ? @(options.maxTokens().value()) : [NSNull null], @"topP": options.topP().has_value() ? @(options.topP().value()) : [NSNull null], @"topK": options.topK().has_value() ? @(options.topK().value()) : [NSNull null], + @"reasoningLevel": options.reasoningLevel() ?: [NSNull null], @"schema": options.schema() ?: [NSNull null], @"tools": options.tools() ?: [NSNull null], }; diff --git a/packages/apple-llm/ios/AppleLLMError.swift b/packages/apple-llm/ios/AppleLLMError.swift index 8b9a7a36..58f8c4b8 100644 --- a/packages/apple-llm/ios/AppleLLMError.swift +++ b/packages/apple-llm/ios/AppleLLMError.swift @@ -18,6 +18,7 @@ enum AppleLLMError: Error, LocalizedError { case toolCallError(Error) case unknownToolCallError case contextWindowExceeded + case rateLimited var errorDescription: String? { switch self { @@ -41,6 +42,8 @@ enum AppleLLMError: Error, LocalizedError { return "Unknown tool call error" case .contextWindowExceeded: return "Context window exceeded" + case .rateLimited: + return "Apple Intelligence request limit reached" } } @@ -65,6 +68,8 @@ enum AppleLLMError: Error, LocalizedError { return "UNKNOWN_TOOL_CALL_ERROR" case .contextWindowExceeded: return "CONTEXT_WINDOW_EXCEEDED" + case .rateLimited: + return "RATE_LIMITED" case .streamNotFound: return nil } @@ -82,6 +87,7 @@ enum AppleLLMError: Error, LocalizedError { case .unknownToolCallError: return 8 case .toolCallError: return 9 case .contextWindowExceeded: return 10 + case .rateLimited: return 11 } } } diff --git a/packages/apple-llm/ios/AppleLLMImpl.swift b/packages/apple-llm/ios/AppleLLMImpl.swift index b1f0aed7..81df11f0 100644 --- a/packages/apple-llm/ios/AppleLLMImpl.swift +++ b/packages/apple-llm/ios/AppleLLMImpl.swift @@ -15,6 +15,14 @@ import FoundationModels public typealias ToolInvoker = @Sendable (String, String, @escaping (Any?, Error?) -> Void) -> Void +#if canImport(FoundationModels) +@available(iOS 26, *) +private enum AppleLanguageModelSelection { + case systemDefault + case privateCloudCompute +} +#endif + @objc public class AppleLLMImpl: NSObject { @@ -77,38 +85,28 @@ public class AppleLLMImpl: NSObject { ) { #if canImport(FoundationModels) if #available(iOS 26, *) { - guard SystemLanguageModel.default.availability == .available else { - rejectWithAppleError(.modelUnavailable, reject: reject) - return - } - Task { do { + let modelSelection = try self.createLanguageModelSelection(from: options) let tools = try self.createTools(from: options, toolInvoker: toolInvoker) let (transcript, userPrompt) = try self.createTranscriptAndPrompt(from: messages, tools: tools) - - let session = LanguageModelSession.init( - model: SystemLanguageModel.default, + let session = try self.createSession( + modelSelection: modelSelection, tools: tools, transcript: transcript ) - let generationOptions = try self.createGenerationOptions(from: options) let generationSchema = try self.createGenerationSchema(from: options) do { - if let generationSchema { - let response = try await session.respond( - to: userPrompt, - schema: generationSchema, - includeSchemaInPrompt: true, - options: generationOptions - ) - resolve(response.toModelMessages()) - } else { - let response = try await session.respond(to: userPrompt, options: generationOptions) - resolve(response.toModelMessages()) - } + let response = try await self.respond( + with: session, + userPrompt: userPrompt, + generationSchema: generationSchema, + generationOptions: generationOptions, + rawOptions: options + ) + resolve(response) } catch { if let appleError = self.mapToAppleLLMError(error, includeGenerationFallback: true) { self.rejectWithAppleError(appleError, reject: reject) @@ -144,42 +142,30 @@ public class AppleLLMImpl: NSObject { ) { #if canImport(FoundationModels) if #available(iOS 26, *) { - guard SystemLanguageModel.default.availability == .available else { - emitStreamError(.modelUnavailable, streamId: streamId, onError: onError) - return - } - let task = Task { do { + let modelSelection = try self.createLanguageModelSelection(from: options) let tools = try self.createTools(from: options, toolInvoker: toolInvoker) let (transcript, userPrompt) = try self.createTranscriptAndPrompt(from: messages, tools: tools) - - let session = LanguageModelSession.init( - model: SystemLanguageModel.default, + let session = try self.createSession( + modelSelection: modelSelection, tools: tools, transcript: transcript ) - let generationOptions = try self.createGenerationOptions(from: options) let generationSchema = try self.createGenerationSchema(from: options) do { - if let generationSchema { - let responseStream = session.streamResponse( - to: userPrompt, - schema: generationSchema, - includeSchemaInPrompt: true, - options: generationOptions - ) - for try await chunk in responseStream { - onUpdate(streamId, String(describing: chunk.content)) + try await self.streamResponse( + with: session, + userPrompt: userPrompt, + generationSchema: generationSchema, + generationOptions: generationOptions, + rawOptions: options, + onUpdate: { content in + onUpdate(streamId, content) } - } else { - let responseStream = session.streamResponse(to: userPrompt, options: generationOptions) - for try await chunk in responseStream { - onUpdate(streamId, chunk.content) - } - } + ) if !Task.isCancelled { onComplete(streamId) @@ -267,6 +253,12 @@ public class AppleLLMImpl: NSObject { return .contextWindowExceeded } +#if compiler(>=6.3) + if case .rateLimited = generationError { + return .rateLimited + } +#endif + return nil } @@ -292,6 +284,216 @@ public class AppleLLMImpl: NSObject { return try Self.createGenerationSchema(fromSchema: schemaOption) } + @available(iOS 26, *) + private func createLanguageModelSelection(from options: [String: Any]) throws -> AppleLanguageModelSelection { + let model = options["model"] as? String ?? "system-default" + + switch model { + case "system-default": + return .systemDefault + case "private-cloud-compute": + return .privateCloudCompute + default: + throw AppleLLMError.generationError("Unsupported Apple language model: \(model)") + } + } + + @available(iOS 26, *) + private func createSession( + modelSelection: AppleLanguageModelSelection, + tools: [any Tool], + transcript: Transcript + ) throws -> LanguageModelSession { + switch modelSelection { + case .systemDefault: + guard SystemLanguageModel.default.availability == .available else { + throw AppleLLMError.modelUnavailable + } + + return LanguageModelSession.init( + model: SystemLanguageModel.default, + tools: tools, + transcript: transcript + ) + case .privateCloudCompute: +#if compiler(>=6.3) + guard #available(iOS 27, *) else { + throw AppleLLMError.unsupportedOS + } + + let model = PrivateCloudComputeLanguageModel() + + guard model.availability == .available else { + throw AppleLLMError.modelUnavailable + } + + return LanguageModelSession.init( + model: model, + tools: tools, + transcript: transcript + ) +#else + throw AppleLLMError.unsupportedOS +#endif + } + } + + @available(iOS 26, *) + private func respond( + with session: LanguageModelSession, + userPrompt: String, + generationSchema: GenerationSchema?, + generationOptions: GenerationOptions, + rawOptions: [String: Any] + ) async throws -> [[String: Any]] { + if hasReasoningLevel(rawOptions) { +#if compiler(>=6.3) + guard #available(iOS 27, *) else { + throw AppleLLMError.unsupportedOS + } + + let contextOptions = try createContextOptions( + from: rawOptions, + includeSchemaInPrompt: generationSchema == nil ? nil : true + ) + + if let generationSchema { + let response = try await session.respond( + to: userPrompt, + schema: generationSchema, + options: generationOptions, + contextOptions: contextOptions + ) + return response.toModelMessages() + } + + let response = try await session.respond( + to: userPrompt, + options: generationOptions, + contextOptions: contextOptions + ) + return response.toModelMessages() +#else + throw AppleLLMError.unsupportedOS +#endif + } + + if let generationSchema { + let response = try await session.respond( + to: userPrompt, + schema: generationSchema, + includeSchemaInPrompt: true, + options: generationOptions + ) + return response.toModelMessages() + } + + let response = try await session.respond(to: userPrompt, options: generationOptions) + return response.toModelMessages() + } + + @available(iOS 26, *) + private func streamResponse( + with session: LanguageModelSession, + userPrompt: String, + generationSchema: GenerationSchema?, + generationOptions: GenerationOptions, + rawOptions: [String: Any], + onUpdate: @escaping (String) -> Void + ) async throws { + if hasReasoningLevel(rawOptions) { +#if compiler(>=6.3) + guard #available(iOS 27, *) else { + throw AppleLLMError.unsupportedOS + } + + let contextOptions = try createContextOptions( + from: rawOptions, + includeSchemaInPrompt: generationSchema == nil ? nil : true + ) + + if let generationSchema { + let responseStream = session.streamResponse( + to: userPrompt, + schema: generationSchema, + options: generationOptions, + contextOptions: contextOptions + ) + for try await chunk in responseStream { + onUpdate(String(describing: chunk.content)) + } + return + } + + let responseStream = session.streamResponse( + to: userPrompt, + options: generationOptions, + contextOptions: contextOptions + ) + for try await chunk in responseStream { + onUpdate(chunk.content) + } + return +#else + throw AppleLLMError.unsupportedOS +#endif + } + + if let generationSchema { + let responseStream = session.streamResponse( + to: userPrompt, + schema: generationSchema, + includeSchemaInPrompt: true, + options: generationOptions + ) + for try await chunk in responseStream { + onUpdate(String(describing: chunk.content)) + } + return + } + + let responseStream = session.streamResponse(to: userPrompt, options: generationOptions) + for try await chunk in responseStream { + onUpdate(chunk.content) + } + } + + @available(iOS 26, *) + private func hasReasoningLevel(_ options: [String: Any]) -> Bool { + return options["reasoningLevel"] is String + } + +#if compiler(>=6.3) + @available(iOS 27, *) + private func createContextOptions( + from options: [String: Any], + includeSchemaInPrompt: Bool? + ) throws -> ContextOptions { + return try ContextOptions( + includeSchemaInPrompt: includeSchemaInPrompt, + reasoningLevel: createReasoningLevel(from: options) + ) + } + + @available(iOS 27, *) + private func createReasoningLevel(from options: [String: Any]) throws -> ContextOptions.ReasoningLevel? { + guard let reasoningLevel = options["reasoningLevel"] as? String else { + return nil + } + + switch reasoningLevel { + case "light": + return .light + case "moderate": + return .moderate + case "deep": + return .deep + default: + throw AppleLLMError.generationError("Unsupported Apple reasoning level: \(reasoningLevel)") + } + } +#endif + @available(iOS 26, *) private static func createGenerationSchema(fromSchema schema: [String: Any]) throws -> GenerationSchema { do { diff --git a/packages/apple-llm/src/NativeAppleLLM.ts b/packages/apple-llm/src/NativeAppleLLM.ts index a5fe6e29..823cdd00 100644 --- a/packages/apple-llm/src/NativeAppleLLM.ts +++ b/packages/apple-llm/src/NativeAppleLLM.ts @@ -10,11 +10,17 @@ export interface AppleMessage { content: string } +export type AppleLanguageModelId = 'system-default' | 'private-cloud-compute' + +export type AppleReasoningLevel = 'deep' | 'light' | 'moderate' + export interface AppleGenerationOptions { + model?: AppleLanguageModelId temperature?: number maxTokens?: number topP?: number topK?: number + reasoningLevel?: AppleReasoningLevel schema?: UnsafeObject tools?: UnsafeObject } diff --git a/packages/apple-llm/src/ai-sdk.ts b/packages/apple-llm/src/ai-sdk.ts index d47e6539..100cb2fa 100644 --- a/packages/apple-llm/src/ai-sdk.ts +++ b/packages/apple-llm/src/ai-sdk.ts @@ -17,13 +17,19 @@ import { generateId, jsonSchema, parseJSON, + parseProviderOptions, Tool as FullToolDefinition, ToolCallOptions, } from '@ai-sdk/provider-utils' +import { z } from 'zod' import { createAppleLLMError, isAppleLLMErrorCode } from './errors' import NativeAppleEmbeddings from './NativeAppleEmbeddings' -import NativeAppleLLM, { type AppleMessage } from './NativeAppleLLM' +import NativeAppleLLM, { + type AppleLanguageModelId, + type AppleMessage, + type AppleReasoningLevel, +} from './NativeAppleLLM' import NativeAppleSpeech from './NativeAppleSpeech' import NativeAppleTranscription from './NativeAppleTranscription' import NativeAppleUtils from './NativeAppleUtils' @@ -31,16 +37,26 @@ import NativeAppleUtils from './NativeAppleUtils' type Tool = LanguageModelV3FunctionTool | LanguageModelV3ProviderTool type ToolDefinitionSet = Record +const appleProviderOptionsSchema = z.object({ + reasoningLevel: z.enum(['deep', 'light', 'moderate']).optional(), +}) + +export interface AppleProviderOptions { + reasoningLevel?: AppleReasoningLevel +} + export function createAppleProvider({ availableTools, }: { availableTools?: ToolDefinitionSet } = {}) { - const createLanguageModel = () => { - return new AppleLLMChatLanguageModel(availableTools) + const createLanguageModel = ( + modelId: AppleLanguageModelId = 'system-default' + ) => { + return new AppleLLMChatLanguageModel(modelId, availableTools) } - const provider = function () { - return createLanguageModel() + const provider = function (modelId: AppleLanguageModelId = 'system-default') { + return createLanguageModel(modelId) } provider.isAvailable = () => NativeAppleLLM.isAvailable() provider.languageModel = createLanguageModel @@ -234,11 +250,15 @@ class AppleLLMChatLanguageModel implements LanguageModelV3 { readonly supportedUrls = {} readonly provider = 'apple' - readonly modelId = 'system-default' + readonly modelId: AppleLanguageModelId private tools: ToolDefinitionSet = {} - constructor(availableTools: ToolDefinitionSet = {}) { + constructor( + modelId: AppleLanguageModelId = 'system-default', + availableTools: ToolDefinitionSet = {} + ) { + this.modelId = modelId this.updateTools(availableTools) } @@ -295,9 +315,22 @@ class AppleLLMChatLanguageModel implements LanguageModelV3 { this.tools = tools } + private async prepareProviderOptions( + options: LanguageModelV3CallOptions + ): Promise { + return ( + (await parseProviderOptions({ + provider: this.provider, + providerOptions: options.providerOptions, + schema: appleProviderOptionsSchema, + })) ?? {} + ) + } + async doGenerate(options: LanguageModelV3CallOptions) { const messages = this.prepareMessages(options.prompt) const tools = this.prepareTools(options.tools) + const providerOptions = await this.prepareProviderOptions(options) for (const tool of tools) { globalThis.__APPLE_LLM_TOOLS__[tool.id] = tool.execute @@ -305,10 +338,12 @@ class AppleLLMChatLanguageModel implements LanguageModelV3 { try { const response = await NativeAppleLLM.generateText(messages, { + model: this.modelId, maxTokens: options.maxOutputTokens, temperature: options.temperature, topP: options.topP, topK: options.topK, + reasoningLevel: providerOptions.reasoningLevel, tools, schema: options.responseFormat?.type === 'json' @@ -365,6 +400,7 @@ class AppleLLMChatLanguageModel implements LanguageModelV3 { async doStream(options: LanguageModelV3CallOptions) { const messages = this.prepareMessages(options.prompt) const tools = this.prepareTools(options.tools) + const providerOptions = await this.prepareProviderOptions(options) if (typeof ReadableStream === 'undefined') { throw new Error( @@ -387,6 +423,7 @@ class AppleLLMChatLanguageModel implements LanguageModelV3 { let streamId: string | null = null let listeners: { remove(): void }[] = [] + const modelId = this.modelId const cleanup = () => { listeners.forEach((listener) => listener.remove()) @@ -472,10 +509,12 @@ class AppleLLMChatLanguageModel implements LanguageModelV3 { listeners = [updateListener, completeListener, errorListener] NativeAppleLLM.generateStream(streamId, messages, { + model: modelId, maxTokens: options.maxOutputTokens, temperature: options.temperature, topP: options.topP, topK: options.topK, + reasoningLevel: providerOptions.reasoningLevel, tools, schema, }) diff --git a/packages/apple-llm/src/errors.ts b/packages/apple-llm/src/errors.ts index 826bd0e1..fb7ab89a 100644 --- a/packages/apple-llm/src/errors.ts +++ b/packages/apple-llm/src/errors.ts @@ -12,6 +12,7 @@ export const AppleLLMErrorCodes = { ToolCallError: 'TOOL_CALL_ERROR', UnknownToolCallError: 'UNKNOWN_TOOL_CALL_ERROR', ContextWindowExceeded: 'CONTEXT_WINDOW_EXCEEDED', + RateLimited: 'RATE_LIMITED', } as const export type AppleLLMErrorCode = diff --git a/packages/apple-llm/src/index.ts b/packages/apple-llm/src/index.ts index 0e73c5e5..e45dff61 100644 --- a/packages/apple-llm/src/index.ts +++ b/packages/apple-llm/src/index.ts @@ -1,8 +1,13 @@ +export type { AppleProviderOptions } from './ai-sdk' export { apple, createAppleProvider } from './ai-sdk' export { default as AppleFoundationModels } from './AppleFoundationModels' export type { AppleLLMError, AppleLLMErrorCode } from './errors' export { AppleLLMErrorCodes } from './errors' export { default as AppleEmbeddings } from './NativeAppleEmbeddings' +export type { + AppleLanguageModelId, + AppleReasoningLevel, +} 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..2cf3dc4c 100644 --- a/skills/react-native-ai/references/apple-provider.md +++ b/skills/react-native-ai/references/apple-provider.md @@ -26,7 +26,7 @@ const result = await generateText({ - [ ] React Native New Architecture - [ ] iOS 26+ (Android not supported) - [ ] Apple Intelligence enabled device -- [ ] Vercel AI SDK v5+ (`ai`) +- [ ] Vercel AI SDK v6+ (`ai`) - [ ] Android or iOS ## Step-by-Step Instructions @@ -65,6 +65,12 @@ const apple = createAppleProvider({ availableTools: tools }) const model = apple.languageModel() ``` +Use Private Cloud Compute with an iOS 27 SDK build when the app has Apple's managed PCC entitlement and needs a larger context window or reasoning: + +```ts +const model = apple.languageModel('private-cloud-compute') +``` + ## Common Pitfalls - **Wrong iOS version**: Apple Intelligence requires iOS 26+. diff --git a/website/src/docs/apple/generating.md b/website/src/docs/apple/generating.md index 07fda3c6..3e4e0231 100644 --- a/website/src/docs/apple/generating.md +++ b/website/src/docs/apple/generating.md @@ -6,6 +6,7 @@ You can generate response using Apple Foundation Models with the Vercel AI SDK's - **iOS 26+** - Apple Foundation Models is available in iOS 26 or later - **Apple Intelligence enabled device** - Device must support Apple Intelligence +- **iOS 27+ for Private Cloud Compute** - PCC requires iOS 27 or later, a native build compiled with the iOS 27 SDK, an Apple Intelligence device, and Apple's managed Private Cloud Compute entitlement ## Text Generation @@ -19,6 +20,42 @@ const result = await generateText({ }); ``` +## Model Selection + +By default, `apple()` uses the on-device `SystemLanguageModel.default` model: + +```typescript +const result = await generateText({ + model: apple(), + prompt: 'Summarize this note' +}); +``` + +Use `apple('private-cloud-compute')` to route generation through Apple's Private Cloud Compute model. PCC is useful for larger context windows and stronger reasoning, but it requires iOS 27+, network access, Apple's managed entitlement, and the user's daily PCC quota. + +```typescript +const result = await generateText({ + model: apple('private-cloud-compute'), + prompt: 'Analyze this long document' +}); +``` + +You can set PCC reasoning with AI SDK provider options: + +```typescript +const result = await generateText({ + model: apple('private-cloud-compute'), + prompt: 'Compare these project plans and recommend the safest rollout', + providerOptions: { + apple: { + reasoningLevel: 'moderate' + } + } +}); +``` + +Supported reasoning levels are `'light'`, `'moderate'`, and `'deep'`. Reasoning uses Apple's iOS 27 `ContextOptions`, so it is unavailable on iOS 26 even when the on-device system model is available. + ## Streaming ```typescript @@ -154,9 +191,12 @@ console.log(result.toolResults); ### Tool calling with structured output -You can also use [`experimental_output`](https://v5.ai-sdk.dev/docs/reference/ai-sdk-core/generate-text#experimental_output) to generate structured output with `generateText`. This is useful when you want to perform tool calls at the same time. +You can also use [`Output.object`](https://ai-sdk.dev/docs/ai-sdk-core/generating-structured-data) to generate structured output with `generateText`. This is useful when you want to perform tool calls at the same time. ```typescript +import { Output, generateText } from 'ai'; +import { z } from 'zod'; + const response = await generateText({ model: apple(), system: `Help the person with getting weather information.`, @@ -164,7 +204,7 @@ const response = await generateText({ tools: { getWeather, }, - experimental_output: Output.object({ + output: Output.object({ schema: z.object({ weather: z.string(), city: z.string(), @@ -205,7 +245,7 @@ if (!apple.isAvailable()) { ## 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. +The on-device Apple Foundation Model has a fixed context window of 4096 tokens. The Private Cloud Compute model has a larger 32K context window. These limits apply to the full request context, including system instructions, previous conversation messages, tool definitions, schemas, and the current user prompt. The `maxTokens` option only limits how many tokens the model can generate in its response. It does not increase the available context window or reserve enough room for a long prompt. @@ -316,6 +356,7 @@ Only documented `AppleLLMErrorCodes.*` values are stable: - `AppleLLMErrorCodes.ToolCallError` / `TOOL_CALL_ERROR` - `AppleLLMErrorCodes.UnknownToolCallError` / `UNKNOWN_TOOL_CALL_ERROR` - `AppleLLMErrorCodes.ContextWindowExceeded` / `CONTEXT_WINDOW_EXCEEDED` +- `AppleLLMErrorCodes.RateLimited` / `RATE_LIMITED` These codes mean: @@ -328,6 +369,7 @@ These codes mean: - `TOOL_CALL_ERROR`: a tool execution failed. - `UNKNOWN_TOOL_CALL_ERROR`: tool execution finished in an unusable or unexpected state. - `CONTEXT_WINDOW_EXCEEDED`: the request exceeded the model context window. +- `RATE_LIMITED`: the request hit Apple's per-user model quota, most commonly with Private Cloud Compute. Errors that do not have a recognized public Apple LLM code may still be thrown, but they are plain `Error` values and are not part of the stable Apple provider API. @@ -426,6 +468,7 @@ Configure model behavior with generation options: - `maxTokens`: Maximum number of tokens to generate - `topP` (0-1): Nucleus sampling threshold - `topK`: Top-K sampling parameter +- `providerOptions.apple.reasoningLevel`: PCC reasoning level. Supported values: `'light'`, `'moderate'`, `'deep'` You can pass selected options with either `generateText` or `generateObject` as follows: @@ -461,6 +504,16 @@ const options = { temperature: 0.7, maxTokens: 100 } const result = await AppleFoundationModels.generateText(messages, options) ``` +You can also select the PCC model and reasoning directly: + +```tsx +const result = await AppleFoundationModels.generateText(messages, { + model: 'private-cloud-compute', + reasoningLevel: 'moderate', + maxTokens: 100, +}) +``` + On iOS 26.4 and newer, you can also count the number of tokens in a string before sending it to the model: @@ -477,5 +530,6 @@ guarantee that a generation request will fit in the model context window. The full context also includes instructions, previous messages in the transcript, tools, schemas, and generated output. -The maximum context window size for Apple's Foundation models is 4096 tokens -per session. More information can be found [here](https://developer.apple.com/documentation/technotes/tn3193-managing-the-on-device-foundation-model-s-context-window). +The maximum context window size for the on-device Apple Foundation Model is +4096 tokens per session. Private Cloud Compute supports a 32K context window. +More information can be found [here](https://developer.apple.com/documentation/technotes/tn3193-managing-the-on-device-foundation-model-s-context-window). diff --git a/website/src/docs/apple/getting-started.md b/website/src/docs/apple/getting-started.md index ae7a9b73..768b811d 100644 --- a/website/src/docs/apple/getting-started.md +++ b/website/src/docs/apple/getting-started.md @@ -10,7 +10,7 @@ Install the Apple provider: npm install @react-native-ai/apple ``` -While you can use the Apple provider standalone, we recommend using it with the Vercel AI SDK for a much better developer experience. The AI SDK provides unified APIs, streaming support, and advanced features. To use with the AI SDK, you'll need v5 and [required polyfills](../polyfills.md): +While you can use the Apple provider standalone, we recommend using it with the Vercel AI SDK for a much better developer experience. The AI SDK provides unified APIs, streaming support, and advanced features. To use with the AI SDK, you'll need v6 and [required polyfills](../polyfills.md): ```bash npm install ai