Multi-Provider AI Architecture for Apps: Don't Marry One Model Vendor
You shipped an AI feature. It calls one model from one vendor, the prompt was tuned against that model, and it works. The model ID is a string constant somewhere in your code, and that string is attached to three things you do not control: a published date on which the model stops answering, infrastructure that can change what it says without the ID changing, and outages you cannot route around. A multi-provider AI architecture is how you stop that string from being a single point of failure.
Each of those risks has a dated primary source, and we start there, for whoever owns the risk. The second half is for the Swift developers who will build the fix. WWDC26 changed part of this picture on Apple platforms, and we will be precise about which part.
Why a single model vendor is a risk you can date
Model providers retire models on published schedules. Anthropic's model deprecations page moves each model through four states (Active, Legacy, Deprecated, Retired) and lists the dates. A selection from that table:
- Claude 3.5 Sonnet: deprecation notice August 13, 2025, retired October 28, 2025.
- Claude 3.7 Sonnet: notice October 28, 2025, retired February 19, 2026.
- Claude 3.5 Haiku: notice December 19, 2025, retired February 19, 2026.
- Claude Sonnet 4 and Opus 4 (the 20250514 snapshots): notice April 14, 2026, retired June 15, 2026.
- Claude Opus 4.1: notice June 5, 2026, retired August 5, 2026.
Anthropic commits to at least 60 days' notice before retiring a publicly released model. That is plenty if someone is watching the page and the switch is cheap. It is not much if the switch means re-tuning prompts and shipping a new binary through App Review.
Two details on the same page matter more than the dates. First, the schedule covers Anthropic-operated platforms only: Amazon Bedrock and Google Cloud Vertex AI set their own retirement dates for the same model. Second, parameters retire too: on Claude Opus 4.7 and later, a non-default temperature, top_p or top_k returns a 400 error.
OpenAI's deprecations page reads the same way. It lists a batch of legacy snapshots, including gpt-4-turbo, gpt-4o-2024-05-13, o1 and o3-mini, with a shutdown date of October 23, 2026: five days before this article was published.
A pinned model is not a stable model
Retirement is the risk you can see coming. The harder one is a model that changes while its ID stays the same. On September 17, 2025, Anthropic published a postmortem of three infrastructure bugs that degraded Claude's output for weeks that summer:
- A routing error sent some requests to servers configured for the 1M-token context window. At its worst hour, on August 31, it affected 16% of Claude Sonnet 4 requests.
- A server misconfiguration corrupted output from Opus 4, Opus 4.1 and Sonnet 4 with stray characters and syntax errors.
- A miscompilation of an approximate top-k operation in the XLA:TPU compiler affected Claude Haiku 3.5, and possibly Sonnet 4 and Opus 3.
No model version changed. Anthropic's own words:
We never reduce model quality due to demand, time of day, or server load. The problems our users reported were due to infrastructure bugs alone.
Anthropic, A postmortem of three recent issues (September 17, 2025)
We take that at face value, and it is exactly the point. Pinning a snapshot protects you from a version change, not a quality change. The only way to notice one is to measure your own feature's output, which is what evals are for.
Then there is plain downtime. OpenAI's status page records a full outage on November 18, 2025, lasting about three hours, that blocked login to both the API platform and ChatGPT. OpenAI attributed it to one of its own third-party service providers. Your vendor has vendors.
Anthropic Engineering: A postmortem of three recent issues
What WWDC26 changed: Foundation Models as a common interface
At WWDC25 Apple shipped the Foundation Models framework as the way to call its on-device model. At WWDC26 it opened the framework to any model through two protocols. LanguageModel describes a model: its capabilities and the executor that runs it. LanguageModelExecutor does the work, streaming a response to each generation request. Apple's own conformances are SystemLanguageModel (on device) and PrivateCloudComputeLanguageModel. Both protocols require iOS, iPadOS, Mac Catalyst, macOS, visionOS or watchOS 27. Once a provider ships a conforming package, the session is created the same way for every model:
import FoundationModels
// On-device, as since iOS 26:
let local = LanguageModelSession()
// Any conforming provider, iOS 27 and later:
let remote = LanguageModelSession(
model: MyCustomServerLanguageModel()
)Two vendors shipped conforming packages that week. Anthropic announced ClaudeForFoundationModels on June 8, 2026: a Swift package whose ClaudeLanguageModel drops into LanguageModelSession(model:). Google announced a GeminiLanguageModel through Firebase AI Logic on June 9, 2026, with @Generable types, tools and SwiftUI call sites unchanged.
What that standardises is the call shape: opening a session, sending a prompt, streaming, tools and structured output. That is a real gain; the view layer no longer needs to know the vendor. It does not standardise the rest:
- Where the model runs. ClaudeLanguageModel and GeminiLanguageModel are network calls to Anthropic's and Google's servers. Conforming to Apple's protocol does not make a third-party model on-device, private in Apple's sense, or free.
- Keys, billing and usage. You still hold an account with each vendor and pay each one. Anthropic's README calls its API-key auth mode development-only and tells production apps to use a proxied mode so no key ships in the binary. Google's routes through Firebase with App Check. Both are telling you to run something between the app and the model.
- Whether a new model is better at your task. The protocol makes a swap one line, not a safe one.
- Older operating systems. Below iOS 27 and macOS 27 the protocols do not exist, so users on those versions need another path.
So WWDC26 moved one layer of the architecture into the platform. The rest is still yours.
WWDC26: Bring an LLM provider to the Foundation Models framework
The multi-provider AI architecture we use
Five pieces, roughly in the order a team needs them. None is exotic. Together they make the model a configuration choice.
A provider-neutral request and response
Features describe what they need in your own types, never in a vendor's. The request says: here is the conversation, these tools exist, the answer must match this schema, stop after this many tokens. An adapter translates. Same discipline as a module boundary: if feature code cannot import a vendor SDK, it cannot quietly depend on one.
// Illustrative: our own types, not an SDK API.
struct Capabilities: OptionSet, Sendable {
let rawValue: Int
static let tools = Capabilities(rawValue: 1)
static let structuredOutput =
Capabilities(rawValue: 2)
static let streaming = Capabilities(rawValue: 4)
}
struct ModelRequest: Sendable {
var feature: String
var system: String?
var messages: [ChatMessage]
var tools: [ToolSpec] = []
var outputSchema: OutputSchema? = nil
var maxOutputTokens: Int
var needs: Capabilities {
var set: Capabilities = []
if !tools.isEmpty { set.insert(.tools) }
if outputSchema != nil {
set.insert(.structuredOutput)
}
return set
}
}
protocol ModelProvider: Sendable {
var name: String { get }
var capabilities: Capabilities { get }
var contextTokens: Int { get }
func respond(to request: ModelRequest)
async throws -> ModelResponse
}On iOS 27 and later, one conformance can be a thin wrapper around a Foundation Models session. Apple's API becomes one implementation behind your seam, not the seam itself, which keeps older OS versions and your Android app on the same contract.
Scaling Swift projects with modular packages
A server-side gateway: keys never in the app
Provider keys live on a server you run, never in the app. Anything compiled into a binary can be read by anyone who downloads it, and a leaked model key is a bill somebody else runs up in your name. The app authenticates to your gateway, which holds the vendor keys.
The gateway still needs to know the caller is your app. App Attest gives it cryptographic evidence that a request came from a genuine build of your app on Apple hardware, the same job App Check does in Google's integration. It proves the app is yours, not that the user is honest, so per-account quotas still belong on the server. The on-device model is the exception: no key, no network, a client-side provider.
iOS app security for businesses: App Attest and the untrusted client
The gateway is also where the next three pieces live, and it changes without an App Store release. That is the operational payoff: a model retirement becomes a configuration change on the server, not a binary waiting in review while the old model stops answering.
Vapor vs Hummingbird: choosing a framework for the gateway
Capability negotiation
Models are not interchangeable, and the differences are the ones that break features: tool calling, structured output, context window size, which request parameters are accepted. Record each provider's capabilities as data and have the gateway match a request's needs against them, instead of assuming. Apple built the same idea into its protocol: every LanguageModel exposes a capabilities property.
Our open-source Swift SDK, Swiftly AIKit, handles one case of this. Its ProviderProtocol carries a supportsTools flag that defaults to true. Perplexity's provider overrides it to false, and the gateway then drops any tools on the request rather than sending them to a model that cannot use them. Whether to drop or fail is a per-feature decision: a summary survives without tools, a booking flow does not.
Fallback and routing
Routing is a list per feature, not a global default: a primary model, then fallbacks, filtered by capability. Fallback should fire only on errors that mean the provider is unavailable, such as timeouts, server errors and rate limiting. A 400 from a retired parameter is your bug, and silently falling back hides it.
// Illustrative: our own types, not an SDK API.
func respond(
to request: ModelRequest,
via chain: [any ModelProvider]
) async throws -> ModelResponse {
let capable = chain.filter {
$0.capabilities.isSuperset(of: request.needs)
}
var lastError: Error = GatewayError.noProvider
for provider in capable {
do {
return try await provider.respond(
to: request
)
} catch let error as ProviderError
where error.isAvailabilityFailure {
lastError = error // try the next one
}
}
throw lastError
}Any other error propagates, which is what you want. Two cautions. A fallback model's output differs from the primary's, so it must pass the same evals before it enters the chain. And two models from one vendor do not survive that vendor's outage: the second entry should come from someone else.
Cost and usage tracking
Log every call at the gateway: feature, provider, model, input and output tokens, latency and prompt version. Vendors bill per token at prices that move on dates you do not choose, and each dashboard shows only its own traffic. A per-feature record lets you answer "what does this feature cost per active user" and "did last month's switch help" with a number.
struct UsageRecord: Codable, Sendable {
let feature: String
let provider: String
let model: String
let promptVersion: String
let inputTokens: Int
let outputTokens: Int
let latencyMilliseconds: Int
let fellBack: Bool
}A real example: Swiftly AIKit
For an open example of this shape in Swift, Swiftly AIKit puts a single AIGateway actor in front of a ProviderProtocol with twelve provider types behind it: OpenAI, Anthropic, Google, Perplexity, Cohere, Mistral, DeepSeek, Grok, Groq, OpenRouter, Ollama and Apple Intelligence. Where keys come from is an explicit APIKeyStrategy: one company key, a key taken from the client's request, a hybrid of the two, or one key per provider through Configuration.withProviderKeys.
Two honest notes. Its README still says eight providers while the source has twelve: documentation drifts from code. And its Apple Intelligence provider calls the on-device model through the iOS 26 LanguageModelSession() API. It predates the WWDC26 LanguageModel protocol and does not use it.
Swapping a model safely: evals before switches
The protocol and the gateway make a switch cheap. Evals make it safe. The order matters: a cheap switch without evals only lets you break production faster.
- Build a small golden set per feature from real inputs, including the awkward ones: long inputs, other languages, the case behind last quarter's support ticket. Strip personal data first.
- Write down what good looks like for each case. For structured output that is mechanical: does it parse, are required fields present. For free text, a short rubric a person can apply in seconds.
- Run the current model and the candidate on the same set and prompt version, and compare side by side, with latency and tokens from the usage log.
- Change the prompt if the new model needs it, and version it. A model swap and a prompt change shipped together are two changes pretending to be one.
- Roll out in stages at the gateway: a small share of traffic, watch parse failures and fallbacks, then widen. Keep the old route one configuration change away.
- Run the golden set on a schedule against the production model, not only before switches. That is how you would have seen the summer 2025 degradation in your own feature instead of in a postmortem.
When one provider is fine
Not every app needs all of this. There are honest cases where one provider is the right call:
- A prototype. You are testing whether anyone wants the feature. Wire it straight to one model and build the gateway when the answer is yes.
- A single low-stakes feature. If an afternoon of downtime costs nothing, a second provider costs more than it saves. Keep the key on a server anyway.
- On-device only. If the feature runs on Apple's SystemLanguageModel, there is no key, no per-token bill and no network. Model changes then arrive with OS releases, which you already test against. What you still need is a plan for devices and OS versions where the model is not available.
Even then, keep the seam. A protocol in front of one provider costs little, and it turns the later switch from a rewrite into a new adapter.
What this cannot tell you
- How often your provider will fail you. We cite one dated outage and one postmortem, not a rate. Third-party outage tallies exist, but we could not check their methodology, so we left them out.
- Whether multi-provider saves money. That depends on your volume and on prices that move.
- How the WWDC26 providers behave under production load. The protocol names and OS requirements come from Apple's reference documentation. The Claude and Gemini packages are a few months old, and we have not run them at scale.
- Where each vendor processes your data. That differs by vendor, product and contract, and it changes. Check the vendor's own documentation.
A practical checklist
- No provider key in any app binary. Keys on a server you run, and App Attest on the endpoints that call models.
- Feature code talks to your own request and response types, never to a vendor SDK.
- Capabilities recorded as data, matched against what each request needs.
- A routing list per feature, with a fallback from a different vendor for availability errors only.
- A golden set per feature, run before every switch and on a schedule after.
- Someone subscribed to each vendor's deprecation page, with retirement dates in the team calendar.
- On iOS 27 and macOS 27, Foundation Models as one implementation behind your protocol, not a replacement for it.
WWDC26 made the call site the same for every model on Apple platforms. That was the easy part to standardise. Where the model runs, who holds the key, what it costs and whether it still does your job next month are architecture decisions. They stay yours, whichever vendor is ahead this quarter.
Comments
Loading comments…