Files
oai-swift/oAI/Providers/ProviderRegistry.swift
T
runeandClaude Sonnet 5 30fbb4162e Revive Apple Intelligence provider (Phase 1 — on-device)
Apple Intelligence / Foundation Models is now genuinely available on
this machine (macOS 27 beta 4) — it was reverted back in June
(f63226b) when it wasn't. Ports the reverted AppleFoundationProvider
forward onto 2.4.3, adapted for everything that's changed since:
PolyForm license headers, current AIProvider protocol shape, current
Settings.Provider/ProviderRegistry/CreditsView/SettingsView structure.

Fixes a real bug found via live testing: LanguageModelSession.
GenerationError was deprecated in macOS 27.0 in favor of a new
LanguageModelError type. On a macOS 27+ runtime, generation failures
now throw LanguageModelError, not GenerationError, so the original
error-mapping catch never matched and Apple's raw error text leaked
to the user instead of oAI's friendly message. Now dispatches to
whichever type the runtime actually throws, gated with
@available(macOS 27.0, *), keeping the old GenerationError path as
a fallback for macOS 26.x (the app's actual deployment target).

Confirmed end-to-end in the live app: provider selectable, Settings
shows a live "Available" badge, chat header shows correct branding,
and — a real, expected Phase 1 limitation — oAI's default system
prompt (active Agent Skills + MCP tool guidance, ~16K tokens on this
machine) exceeds the on-device model's 4K context window on the
very first message. The friendly error message now correctly reports
this instead of Apple's raw string. Tool calling remains out of scope
until Phase 3.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-24 09:46:17 +02:00

120 lines
3.7 KiB
Swift

//
// ProviderRegistry.swift
// oAI
//
// Registry for managing multiple AI providers
//
// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
// Copyright (C) 2026 Rune Olsen
//
// This file is part of oAI.
//
// oAI is licensed under the PolyForm Noncommercial License 1.0.0.
// You may use, study, modify, and share it for any noncommercial
// purpose. Commercial use — including selling oAI or any part of
// it, standalone or bundled into another product or service —
// requires a separate commercial license from the copyright holder.
//
// See the LICENSE file or
// <https://polyformproject.org/licenses/noncommercial/1.0.0> for
// the full license text. For commercial licensing, contact Rune
// Olsen via <https://oai.pm>.
import Foundation
import os
class ProviderRegistry {
static let shared = ProviderRegistry()
private var providers: [Settings.Provider: AIProvider] = [:]
private let settings = SettingsService.shared
private init() {}
// MARK: - Get Provider
func getProvider(for providerType: Settings.Provider) -> AIProvider? {
// Return cached provider if exists
if let provider = providers[providerType] {
return provider
}
// Create new provider based on type
let provider: AIProvider?
switch providerType {
case .openrouter:
guard let apiKey = settings.openrouterAPIKey, !apiKey.isEmpty else {
Log.api.warning("No API key configured for OpenRouter")
return nil
}
provider = OpenRouterProvider(apiKey: apiKey)
case .anthropic:
guard let apiKey = settings.anthropicAPIKey, !apiKey.isEmpty else {
Log.api.warning("No API key configured for Anthropic")
return nil
}
provider = AnthropicProvider(apiKey: apiKey)
case .openai:
guard let apiKey = settings.openaiAPIKey, !apiKey.isEmpty else {
Log.api.warning("No API key configured for OpenAI")
return nil
}
provider = OpenAIProvider(apiKey: apiKey)
case .ollama:
provider = OllamaProvider(baseURL: settings.ollamaEffectiveURL)
case .appleOnDevice:
provider = AppleFoundationProvider()
}
// Cache and return
if let provider = provider {
Log.api.info("Created \(providerType.rawValue) provider")
providers[providerType] = provider
}
return provider
}
// MARK: - Current Provider
func getCurrentProvider() -> AIProvider? {
let currentProviderType = settings.defaultProvider
return getProvider(for: currentProviderType)
}
// MARK: - Clear Cache
func clearCache() {
providers.removeAll()
}
// MARK: - Validate API Key
func hasValidAPIKey(for providerType: Settings.Provider) -> Bool {
switch providerType {
case .openrouter:
return settings.openrouterAPIKey != nil && !settings.openrouterAPIKey!.isEmpty
case .anthropic:
return AnthropicOAuthService.shared.isAuthenticated
|| (settings.anthropicAPIKey != nil && !settings.anthropicAPIKey!.isEmpty)
case .openai:
return settings.openaiAPIKey != nil && !settings.openaiAPIKey!.isEmpty
case .ollama:
return settings.ollamaConfigured
case .appleOnDevice:
return true // no API key needed
}
}
/// Providers that have credentials configured (API key or, for Ollama, a saved URL)
var configuredProviders: [Settings.Provider] {
Settings.Provider.allCases.filter { hasValidAPIKey(for: $0) }
}
}