feat: WWDC26 Apple Intelligence APIs#218
Conversation
|
@JKobrynski is attempting to deploy a commit to the Callstack Team on Vercel. A member of the Team first needs to authorize it. |
There was a problem hiding this comment.
Pull request overview
Exposes Apple Foundation Models “history management” utilities (summarization, rolling window, completed tool-call pruning) as fluent JS model wrappers in @react-native-ai/apple, adds native support for model info + Image Playground, and extends the Expo example app to toggle/demo the new behaviors.
Changes:
- Add fluent Apple language model wrappers (
summarizeHistory,rollingWindow,droppingCompletedToolCalls) and apply transforms before bothdoGenerateanddoStream. - Add native APIs for
getModelInfoandgenerateImages, plus JS exports/types and updated docs/README. - Add “Apple History Demo” toggles + sliders in the Expo example app and preserve existing chat behavior when disabled.
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| website/src/docs/apple/generating.md | Updates Apple provider docs (availability, PCC, images, history helpers, error handling). |
| packages/apple-llm/src/NativeAppleLLM.ts | Extends TurboModule typings to support attachments/providerOptions + new native methods. |
| packages/apple-llm/src/index.ts | Exports new Apple provider types + trimAppleMessagesForContext. |
| packages/apple-llm/src/AppleFoundationModels.ts | Adds JS fallbacks/wrappers for getModelInfo and generateImages. |
| packages/apple-llm/src/ai-sdk.ts | Implements fluent history wrappers, image model, prompt attachment handling, and model-info API. |
| packages/apple-llm/README.md | Documents new features and requirements + shows fluent history usage. |
| packages/apple-llm/ios/AppleLLMImpl.swift | Implements getModelInfo, Image Playground generation, attachments, PCC, Vision tools plumbing. |
| packages/apple-llm/ios/AppleLLMError.swift | Refines invalid-message error shape/message. |
| packages/apple-llm/ios/AppleLLM.mm | Bridges new options + adds getModelInfo/generateImages TurboModule methods. |
| apps/expo-example/src/store/chatStore.ts | Adds persisted settings for the Apple History Demo. |
| apps/expo-example/src/screens/ChatScreen/SettingsSheet.tsx | Adds History Demo toggle + sliders for window/threshold. |
| apps/expo-example/src/screens/ChatScreen/index.tsx | Applies fluent history wrappers when demo enabled; improves empty state text plumbing. |
| apps/expo-example/src/screens/ChatScreen/ChatMessages.tsx | Accepts precomputed empty-state subtitle from parent. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| function isToolRelatedMessage(message: LanguageModelV3Message) { | ||
| if (message.role === 'tool') { | ||
| return true | ||
| } | ||
|
|
||
| if (message.role !== 'assistant') { | ||
| return false | ||
| } | ||
|
|
||
| return message.content.some( | ||
| (part) => part.type === 'tool-call' || part.type === 'tool-result' | ||
| ) | ||
| } |
There was a problem hiding this comment.
Let's take a look at this
There was a problem hiding this comment.
Good catch. This is handled now by checking Array.isArray(message.content) before looking for tool-call / tool-result parts. If the assistant content is a plain string, we now return false, so it is preserved and won’t be treated as a completed tool-call message.
| export interface AppleContextOptions { | ||
| /** | ||
| * Keep the first system message and only the last N non-system messages. | ||
| */ | ||
| rollingWindowMessages?: number | ||
| /** | ||
| * Remove completed tool-call/tool-result entries from older context. | ||
| */ | ||
| dropCompletedToolCalls?: boolean | ||
| } |
There was a problem hiding this comment.
Handled by updating the rollingWindowMessages docs to match the implementation: system messages are preserved, and the window applies to the latest non-system conversation messages.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
|
Please have a look at outstanding copilot comments and decide whether to address them or not. Quick comment:
Would that be AI SDK native way, or shall we instead use options via I am more familiar with the latter. |
|
Please do a follow-up after this one is merged to #217 - I guess partial support for private cloud compute is done here, but we also need to do support for "reasoning" option, some errors and potentially other things (exact diff to be checked). |
@grabbou that makes sense. I originally went with fluent wrappers because the task description called out that shape and it maps closely to Apple’s That said, I agree the options-based API is more consistent with the rest of this package and with how Apple-specific behavior is already configured through I’ll update this to expose the history controls through options instead. |
Sure, will do |
|
@artus9033 @grabbou PR updated, feel free to take another look |
Yeah, it's likely the AI research went a bit too far on this one! |
artus9033
left a comment
There was a problem hiding this comment.
LGTM from my end after a few comments resolved, great job!
| function isToolRelatedMessage(message: LanguageModelV3Message) { | ||
| if (message.role === 'tool') { | ||
| return true | ||
| } | ||
|
|
||
| if (message.role !== 'assistant') { | ||
| return false | ||
| } | ||
|
|
||
| return message.content.some( | ||
| (part) => part.type === 'tool-call' || part.type === 'tool-result' | ||
| ) | ||
| } |
There was a problem hiding this comment.
Let's take a look at this
|
Thanks for the work on this 👍 - noticed it adds getModelInfo() for model availability/capabilities, which feels like the natural neighbor for language support, so raising this here before opening a separate PR. Use case: Apple's on-device Foundation Models support a fixed set of locales (~16 as of iOS 27), and notably not all languages. For apps that generate localized output ("respond in the user's language"), there's currently no way to know at runtime whether the model can handle a given locale - so we hardcode Apple's list and keep it in sync manually. Apple exposes this natively via SystemLanguageModel.supportedLanguages doc, but the RN wrapper only bridges isAvailable(). Proposal: expose the supported languages through the provider, e.g.: Question: would you prefer this as a standalone method, or folded into the getModelInfo() surface you're adding here (e.g. getModelInfo().supportedLanguages)? Happy to open a PR once you confirm the preferred shape - just want to avoid conflicting with this one. Or i can wait when yours @JKobrynski will be merged and then create a patch. |
|
Hello @glibenko ! First of all, thanks for the interest in this library. Indeed, it is a sensible design to expose this information. I think you could've missed it in the diffs, but I think this PR already has everything you mean - check here & here. Is that what you had on mind, or are we missing something? |
|
@glibenko I tested this on device through the demo app and confirmed that While testing, I noticed the values were initially coming through as Swift debug descriptions because we were serializing them with |
|
@artus9033 @grabbou addressed everything up to this point, feel free to take another look at the PR |
|
LGTM from my end! @grabbou are we good to merge this one? 🚀 |
|
Yeah, you guys go ahead! |
| threshold: appleHistorySummarizationThreshold, | ||
| model: createAppleProvider({ | ||
| model: modelId, | ||
| }).languageModel(), |
There was a problem hiding this comment.
I think this is wrong/could be improved. The options should not take LanguageModelV3 and then:
const modelId = model.modelId as AppleLanguageModelId
in order to re-create it here with "createAppleProvider".
Since this is not taking ANY model, but specific Apple models, perhaps it's better to provide strict typing on modelId (worst case scenario: string), and just pass it down as this?
I am not sure what are the options, but I guess it's Apple-specific.
There was a problem hiding this comment.
I also wonder, since this is not a built-in Apple feature, shall we rely on it? I think it would be more appropriate to use a separate utility. And then, we can just pass "model" here (and make this work with any model, including Apple and Llama).
I would leave this in user land to be honest and clean-up the code to only rely on Apple features.
grabbou
left a comment
There was a problem hiding this comment.
A few follow-ups from a deeper pass over the non-history surface (model info / image prompts / Image Playground / PCC). The history-management API design is being tracked separately; these are independent of it.
| .appendingPathComponent(UUID().uuidString) | ||
| .appendingPathExtension(fileExtension) | ||
|
|
||
| try imageData.write(to: fileURL, options: .atomic) |
There was a problem hiding this comment.
Temp-file leak. This writes the decoded image bytes to the temp directory but nothing ever deletes the file, so every inline-data image prompt — and every Image Playground source image, since createImagePlaygroundConcepts routes through createImageURL too — leaks a temp file for the lifetime of the app. iOS only purges tmp opportunistically, so a chat that sends images repeatedly will keep growing disk usage.
Suggested path: delete the file once the underlying call has consumed it. Simplest is to collect the URLs created for a request and defer { try? FileManager.default.removeItem(at: url) } after session.respond(...) / creator.images(...) returns. Alternatively write into a per-request subdirectory (temporaryDirectory/<uuid>/) and remove the whole directory at the end.
| #endif | ||
|
|
||
| private func availabilityString(_ availability: Any) -> String { | ||
| return String(describing: availability) |
There was a problem hiding this comment.
availability leaks a Swift debug string. String(describing:) of the availability enum reaches JS as something like unavailable(reason: ...) — the same class of issue already fixed for supportedLanguages in this PR. AppleModelInfo.availability: string ends up opaque and unstable across OS versions, so consumers can't reliably switch on it.
Suggested path: map the cases explicitly to stable identifiers, e.g.
switch availability {
case .available: return "available"
case .unavailable: return "unavailable"
}If the unavailable reason matters, surface it as its own structured field rather than embedding it in a describe-dump.
| "isAvailable": model.availability == .available, | ||
| "availability": availabilityString(model.availability), | ||
| "contextSize": contextSize, | ||
| "quotaUsage": String(describing: model.quotaUsage), |
There was a problem hiding this comment.
quotaUsage is a describe-dump. String(describing: model.quotaUsage) reaches JS as the type's debug description (e.g. QuotaUsage(...)) rather than a usable value — same problem as availability and the already-fixed supportedLanguages.
Suggested path: pull the concrete fields off quotaUsage and return a structured object (or a single normalized number/ratio) the JS side can consume, and type quotaUsage accordingly in AppleModelInfo. If the shape isn't stable enough to model yet, I'd drop it from the payload for now rather than ship an opaque string.
| "quotaUsage": String(describing: model.quotaUsage), | ||
| "supportsLocale": model.supportsLocale(locale), | ||
| "supportedLanguages": model.supportedLanguages.map(languageIdentifier).sorted(), | ||
| "supportsTokenCounting": false, |
There was a problem hiding this comment.
Confirm intent: supportsTokenCounting is hardcoded false for the PCC model while the system model reports true on 26.4+. Intentional (PCC genuinely can't count tokens), or a copy-paste oversight? Consumers will branch on this. If it's deliberate, a one-line comment here noting why would save the next reader the same double-take.
There was a problem hiding this comment.
Good catch. PCC is not actually working/buildable in the current setup, because the SDK we’re compiling against does not expose the required Swift symbols yet. I’m going to leave the PCC implementation unsupported for now rather than add a workaround or pretend the capability is ready.
Once we figure out the broader PCC story and have an SDK surface we can build against reliably, we should revisit this and make the model info shape explicit there. @artus9033 is going to help me with this, I will let you know when we have something!
There was a problem hiding this comment.
Yes, this is a problem we are aware of. I'll try experimenting with it. Worst case, we can cut off backwards compat from the new release or require users to install with an env var.
| return { | ||
| type: 'image', | ||
| mediaType, | ||
| url: value, |
There was a problem hiding this comment.
Remote-URL handling is inconsistent between the two image paths. This returns any non-data: string as a url attachment, including http(s)://, which then fails later in native with the generic "Image attachment URLs must be local file URLs". The image-generation path (prepareImageModelFiles, lines 196-210) already rejects remote URLs early in JS with a clear message — that was the Copilot fix — but this image-prompt path was missed.
Suggested path: add the same early guard here: reject http:///https:// with a clear "remote URLs not supported, provide a local file or data URL" error, and only pass through data:, file://, and absolute local paths. Better still, factor the URL/data normalization into one shared helper used by both prepareImageAttachment and prepareImageModelFiles so the two paths can't drift again.
| supportedLanguages: string[] | ||
| supportsTokenCounting: boolean | ||
| supportsImagePrompts: boolean | ||
| supportsPrivateCloudCompute: boolean |
There was a problem hiding this comment.
Drop supportsPrivateCloudCompute entirely. It's redundant — PCC availability is already answerable via getModelInfo({ model: 'private-cloud-compute' }).isAvailable when the proper model id is used. As implemented it's also misleading: the system model always reports false (native modelInfo(for: SystemLanguageModel...)) even on iOS 27 where PCC is available, because the flag describes the instance, not the OS capability.
Suggested path: remove this field from AppleModelInfo, and remove the supportsPrivateCloudCompute entries from both modelInfo(...) builders in AppleLLMImpl.swift. Consumers determine PCC support by querying the PCC model id directly.
| return createLanguageModel(options) | ||
| } | ||
| provider.isAvailable = () => NativeAppleLLM.isAvailable() | ||
| provider.getModelInfo = (options: { locale?: string; model?: string } = {}) => |
There was a problem hiding this comment.
Tighten the param type. model?: string is looser than the rest of the surface, which uses AppleLanguageModelId (and AppleModelInfo.model is already AppleLanguageModelId).
Suggested path: type it as { locale?: string; model?: AppleLanguageModelId } so callers get autocomplete and an invalid id is caught at compile time instead of failing in native with Unsupported model '...'.
There was a problem hiding this comment.
This is a good suggestion
Related PR - #215
Summary
Builds on the WWDC26 Apple Intelligence API work from #215 and finishes the JS-facing history management API for
@react-native-ai/apple.PR #215 added the broader Apple Intelligence surface, including Apple provider options, model info APIs, iOS 27 Private Cloud Compute support, image prompts, Image Playground generation, Vision built-in tools, and lower-level native helpers. It also introduced context/history utilities internally.
This change exposes those history management utilities as fluent AI SDK model wrappers so consumers can compose Apple history behavior directly from JS:
It also adds demo app controls for testing the behavior without replacing the existing basic chat flow.
Changes
summarizeHistory(threshold, model)rollingWindow(entries)droppingCompletedToolCalls()doGenerateanddoStreamTesting
bun run --filter='@react-native-ai/apple' typecheckbun run --filter='@react-native-ai/example' typecheckbun lintgit diff --check