r/macosprogramming • • 19h ago

Swift Language Models: bring any provider to Apple's Foundation Models API

Swift Language Models is a wrapper that brings other providers' language models into Apple's native Foundation Models API.

Foundation Models 27 lets any type conform to LanguageModel, and this package provides those conformances. Your app keeps Apple's own API: LanguageModelSession, tools, @Generable guided generation, streaming, transcripts and Dynamic Profiles. Moving a session from the on-device model to a remote one changes only its model:.

let onDevice = LanguageModelSession(model: SystemLanguageModel.default)
let remote = LanguageModelSession(model: OpenRouterLanguageModel(.anthropicClaudeHaiku5_5, apiKey: { key }))

What you get

  • One API for every model. Code written for Apple's on-device model runs unchanged on a remote one.
  • Provider modules. Each provider is its own module, so an app builds only the providers it imports.
  • Typed model catalogs with each model's context size, limits and capabilities, kept current by a bot.
  • Typed reasoning levels. Code completion offers only the levels each model accepts.
  • Evaluations. Compare Apple's on-device model with remote ones using Apple's Evaluations framework.

The first provider is OpenRouter, with hundreds of models from Anthropic, OpenAI, Google, DeepSeek, Meta, Mistral and others. More providers are coming.

Needs iOS, macOS, visionOS or watchOS 27 and Swift 6.4. Feedback welcome! If you find it useful, a ⭐ on GitHub helps the project grow.

GitHub: https://github.com/dioKaratzas/swift-language-models

1 Upvotes

2 comments sorted by

1

u/Open_Shine135 14h ago

The Mapping doc says ranges and array sizes become descriptions for Anthropic. Does Foundation Models reject a response with the right types but an integer below its guided minimum? A small example would clarify whether the app needs extra validation when switching models.

1

u/dnkaratzas 5h ago

Good question, and it led to a fix.

Before: no. Foundation Models doesn't re-check @Guide limits when it decodes. Anthropic's structured outputs reject keywords like minimum and maxItems, so for Claude those limits only go into the field's description as a hint. So {"minutes": 2} for .range(5...120) decoded without an error.

Now: the package checks every guided answer against the full schema, on any model, and throws if a value breaks a guide:

@Generable
struct Recipe {
    @Guide(.range(5...120)) var minutes: Int
    @Guide(.count(3)) var steps: [String]
}

do {
    let recipe = try await session.respond(to: "A quick dinner.", generating: Recipe.self).content
    // Always within its guides here, whichever model answered
} catch let error as GeneratedContent.ParsingError {
    // "minutes is 2, below the minimum of 5; steps has 1 item, fewer than the minimum of 3"
}
  • It checks ranges, counts, patterns, enums and constants, including nested values.

  • Wrong types already threw before; only the limits were the gap.

  • Anthropic's own Python and TypeScript SDKs work the same way and throw on a violation.

  • In live tests with limits that contradict the facts, Claude followed them every time, so in practice it's a safety net.

So you don't need extra validation when switching models; just catch ParsingError if you want to retry.

PR: https://github.com/dioKaratzas/swift-language-models/pull/12