Skip to content

apple-llm fails to compile on Xcode 26.2: countTokens uses an iOS 26.4 SDK symbol #227

Description

@MrJune15

Summary

packages/apple-llm cannot be compiled for iOS with Xcode 26.2. AppleLLMImpl.swift calls SystemLanguageModel.default.tokenCount(for:), which is an iOS 26.4 SDK symbol. It does not exist in the iOS 26.2 SDK, so the package fails to build rather than degrading gracefully.

This effectively makes Xcode 26.4+ a hard requirement for any iOS build of the repo, including apps/expo-example, which is not stated anywhere in the README or CONTRIBUTING.md.

Error

❌  (../../packages/apple-llm/ios/AppleLLMImpl.swift:54:66)

  52 |       Task {
  53 |         do {
> 54 |           let tokenCount = try await SystemLanguageModel.default.tokenCount(for: text)
     |                                                                  ^ value of type 'SystemLanguageModel' has no member 'tokenCount'

CommandError: Failed to build iOS project. "xcodebuild" exited with error code 65.

Confirming the symbol is absent from the 26.2 SDK entirely:

$ xcrun --sdk iphoneos --show-sdk-version
26.2
$ sdk=$(xcrun --sdk iphoneos --show-sdk-path)
$ grep -rl "tokenCount" "$sdk/System/Library/Frameworks/FoundationModels.framework/"
(no matches)

Why the availability guard doesn't help

The call is already wrapped in if #available(iOS 26.4, *) (AppleLLMImpl.swift:43):

#if canImport(FoundationModels)
    if #available(iOS 26.4, *) {
      guard SystemLanguageModel.default.availability == .available else { ... }
      Task {
        do {
          let tokenCount = try await SystemLanguageModel.default.tokenCount(for: text)
          resolve(tokenCount)
        } catch { ... }
      }
    } else {
      let error = AppleLLMError.unsupportedOS
      reject("AppleLLM", error.localizedDescription, error)
    }
#else
    ...
#endif

#available gates runtime dispatch — it lets a binary built against a newer SDK run safely on older OS versions. It cannot compile a symbol that the SDK being built against does not declare. #if canImport(FoundationModels) doesn't help either: the framework is importable in 26.2, it just lacks this one method.

Note this is specific to the 26.4 call. Every other gate in the file is iOS 26 / @available(iOS 26, *), which compiles fine against the 26.2 SDK.

Introduced in 9e14139 ("feat: graceful apple context window error handling, countTokens API", #212).

The JS layer already handles this case

src/AppleFoundationModels.ts treats the native method as optional and fails gracefully when it's missing:

countTokens?: (text: string) => Promise<number>
// ...
countTokens: (text) => {
  if (typeof nativeAppleLLM.countTokens !== 'function') {
    throw new Error(
      'Apple Foundation Models token counting is unavailable. It requires iOS 26.4 or newer and a native build with countTokens support.'
    )
  }
  return nativeAppleLLM.countTokens(text)
}

That error message explicitly anticipates "a native build with countTokens support" — i.e. the design already expects builds where the native side omits this method. The Swift side just doesn't implement that half of the contract; it hard-fails at compile time instead of being omitted.

(Minor inconsistency worth a look: the codegen spec at src/NativeAppleLLM.ts:39 declares countTokens(text: string): Promise<number> as required, while the wrapper above declares it optional.)

Suggested fix

Compile the 26.4 path conditionally on the SDK, so older Xcode versions produce a build without the method and the existing JS fallback handles it. One approach — have the podspec detect the SDK and define a flag:

sdk_version = `xcrun --sdk iphoneos --show-sdk-version`.strip
if Gem::Version.new(sdk_version) >= Gem::Version.new('26.4')
  s.pod_target_xcconfig = {
    'OTHER_SWIFT_FLAGS' => '$(inherited) -D APPLE_LLM_HAS_TOKEN_COUNT'
  }
end
#if APPLE_LLM_HAS_TOKEN_COUNT
    if #available(iOS 26.4, *) { /* existing implementation */ }
    else { /* unsupportedOS */ }
#else
    let error = AppleLLMError.unsupportedOS
    reject("AppleLLM", error.localizedDescription, error)
#endif

Alternatively, if requiring Xcode 26.4+ is intentional, documenting it in the README and CONTRIBUTING.md prerequisites would be enough — right now the failure mode is an opaque Swift compile error partway through a long build.

Local workaround

Replacing the body of countTokens with the unsupportedOS rejection lets the package and the example app build and run normally on Xcode 26.2. Only the token-counter readout is lost; text generation, embeddings, transcription and speech are unaffected.

Why CI doesn't catch it

.github/workflows/ci.yml runs cd packages/apple-llm && bun run prepare on macos-latest, but that is a react-native-builder-bob JavaScript build — it never invokes xcodebuild, so the Swift in ios/ is never compiled. A build-only xcodebuild job against apps/expo-example (no device, no signing) would catch this class of problem.

This is the same gap reported in #226, where a second, unrelated defect also reaches main unbuilt.

Environment

  • macOS 15 (Darwin 25.5.0), Apple Silicon
  • Xcode 26.2, iOS SDK 26.2
  • Target device: iPad, iOS 26.6 (the OS supports the API; the SDK is what's behind)
  • Repo at d4ce05f

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions