diff --git a/oAI/Models/Settings.swift b/oAI/Models/Settings.swift
index af46f2c..f2358d2 100644
--- a/oAI/Models/Settings.swift
+++ b/oAI/Models/Settings.swift
@@ -58,25 +58,17 @@ struct Settings: Codable {
case anthropic
case openai
case ollama
- case appleOnDevice = "apple_on_device"
-
+
var displayName: String {
- switch self {
- case .openrouter: return "OpenRouter"
- case .anthropic: return "Anthropic"
- case .openai: return "OpenAI"
- case .ollama: return "Ollama"
- case .appleOnDevice: return "Apple Intelligence"
- }
+ rawValue.capitalized
}
-
+
var iconName: String {
switch self {
- case .openrouter: return "network"
- case .anthropic: return "brain"
- case .openai: return "sparkles"
- case .ollama: return "server.rack"
- case .appleOnDevice: return "apple.logo"
+ case .openrouter: return "network"
+ case .anthropic: return "brain"
+ case .openai: return "sparkles"
+ case .ollama: return "server.rack"
}
}
}
diff --git a/oAI/Providers/AppleFoundationProvider.swift b/oAI/Providers/AppleFoundationProvider.swift
deleted file mode 100644
index 340f0b7..0000000
--- a/oAI/Providers/AppleFoundationProvider.swift
+++ /dev/null
@@ -1,195 +0,0 @@
-//
-// AppleFoundationProvider.swift
-// oAI
-//
-// Apple Foundation Models provider (on-device Apple Intelligence)
-//
-// SPDX-License-Identifier: AGPL-3.0-or-later
-// Copyright (C) 2026 Rune Olsen
-//
-// This file is part of oAI.
-//
-// oAI is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Affero General Public License as
-// published by the Free Software Foundation, either version 3 of the
-// License, or (at your option) any later version.
-//
-// oAI is distributed in the hope that it will be useful, but WITHOUT
-// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
-// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General
-// Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public
-// License along with oAI. If not, see .
-
-
-import Foundation
-import FoundationModels
-import os
-
-final class AppleFoundationProvider: AIProvider {
- let name = "Apple Intelligence"
-
- let capabilities = ProviderCapabilities(
- supportsStreaming: true,
- supportsVision: false,
- supportsTools: false,
- supportsOnlineSearch: false,
- maxContextLength: 4096
- )
-
- // MARK: - Models
-
- func listModels() async throws -> [ModelInfo] {
- [
- ModelInfo(
- id: "apple-on-device",
- name: "Apple On-Device",
- description: "On-device Apple Intelligence model. Private, free, and works offline. 4K context window.",
- contextLength: 4096,
- pricing: ModelInfo.Pricing(prompt: 0, completion: 0),
- capabilities: ModelInfo.ModelCapabilities(
- vision: false,
- tools: false,
- online: false
- )
- )
- ]
- }
-
- func getModel(_ id: String) async throws -> ModelInfo? {
- try await listModels().first { $0.id == id }
- }
-
- func getCredits() async throws -> Credits? { nil }
-
- // MARK: - Streaming chat
-
- func streamChat(request: ChatRequest) -> AsyncThrowingStream {
- AsyncThrowingStream { continuation in
- Task {
- do {
- let session = try self.makeSession(for: request)
- let prompt = self.lastUserMessage(from: request)
-
- // streamResponse(to: String) → ResponseStream
- // Each snapshot.content is the full accumulated text so far (snapshot model).
- // We compute deltas by comparing each snapshot to the previous.
- let stream = session.streamResponse(to: prompt)
- var lastContent = ""
-
- for try await snapshot in stream {
- let current = snapshot.content
- if current.count > lastContent.count {
- let delta = String(current.dropFirst(lastContent.count))
- continuation.yield(StreamChunk(
- id: UUID().uuidString,
- model: request.model,
- delta: StreamChunk.Delta(content: delta, role: "assistant"),
- finishReason: nil,
- usage: nil
- ))
- lastContent = current
- }
- }
-
- continuation.yield(StreamChunk(
- id: UUID().uuidString,
- model: request.model,
- delta: StreamChunk.Delta(content: nil, role: nil),
- finishReason: "stop",
- usage: nil
- ))
- continuation.finish()
-
- } catch let genError as LanguageModelSession.GenerationError {
- continuation.finish(throwing: self.mapGenerationError(genError))
- } catch {
- continuation.finish(throwing: error)
- }
- }
- }
- }
-
- // MARK: - Non-streaming chat
-
- func chat(request: ChatRequest) async throws -> ChatResponse {
- let session = try makeSession(for: request)
- let prompt = lastUserMessage(from: request)
- let response: LanguageModelSession.Response = try await session.respond(to: prompt)
- return ChatResponse(
- id: UUID().uuidString,
- model: request.model,
- content: response.content,
- role: "assistant",
- finishReason: "stop",
- usage: nil,
- created: Date()
- )
- }
-
- // MARK: - Tool messages (not supported in Phase 1)
-
- func chatWithToolMessages(model: String, messages: [[String: Any]], tools: [Tool]?, maxTokens: Int?, temperature: Double?) async throws -> ChatResponse {
- throw ProviderError.unknown("Tool calling requires Apple Foundation Models Phase 3.")
- }
-
- // MARK: - Session construction
-
- private func makeSession(for request: ChatRequest) throws -> LanguageModelSession {
- guard case .available = SystemLanguageModel.default.availability else {
- throw availabilityError()
- }
-
- // Build instructions: system prompt + prior conversation turns as formatted text.
- // Foundation Models sessions don't accept a message array — we inject history inline.
- var instructions = request.systemPrompt ?? ""
- let priorMessages = request.messages.dropLast().filter { $0.role != .system }
-
- if !priorMessages.isEmpty {
- let history = priorMessages
- .map { m -> String in
- let label = m.role == .user ? "User" : "Assistant"
- return "\(label): \(m.content)"
- }
- .joined(separator: "\n")
- instructions += "\n\nConversation so far:\n\(history)\n\nContinue from here."
- }
-
- return instructions.isEmpty
- ? LanguageModelSession()
- : LanguageModelSession(instructions: instructions)
- }
-
- private func lastUserMessage(from request: ChatRequest) -> String {
- request.messages.last(where: { $0.role == .user })?.content ?? ""
- }
-
- // MARK: - Error mapping
-
- private func availabilityError() -> Error {
- switch SystemLanguageModel.default.availability {
- case .unavailable(.deviceNotEligible):
- return ProviderError.unknown("This Mac doesn't support Apple Intelligence. Apple Silicon is required.")
- case .unavailable(.appleIntelligenceNotEnabled):
- return ProviderError.unknown("Apple Intelligence is not enabled. Open System Settings → Apple Intelligence to turn it on.")
- case .unavailable(.modelNotReady):
- return ProviderError.unknown("Apple Intelligence model is still downloading. Please wait and try again.")
- default:
- return ProviderError.unknown("Apple Intelligence is not available on this device.")
- }
- }
-
- private func mapGenerationError(_ error: LanguageModelSession.GenerationError) -> Error {
- switch error {
- case .exceededContextWindowSize:
- return ProviderError.unknown("Apple Intelligence context limit exceeded (4,096 tokens). Start a new chat or enable Progressive Summarization in Settings → Advanced.")
- case .rateLimited:
- return ProviderError.rateLimitExceeded
- case .guardrailViolation:
- return ProviderError.unknown("Apple Intelligence declined to respond to this message.")
- default:
- return error
- }
- }
-}
diff --git a/oAI/Providers/ProviderRegistry.swift b/oAI/Providers/ProviderRegistry.swift
index e0d3aa6..9785a62 100644
--- a/oAI/Providers/ProviderRegistry.swift
+++ b/oAI/Providers/ProviderRegistry.swift
@@ -69,9 +69,6 @@ class ProviderRegistry {
case .ollama:
provider = OllamaProvider(baseURL: settings.ollamaEffectiveURL)
-
- case .appleOnDevice:
- provider = AppleFoundationProvider()
}
// Cache and return
@@ -109,8 +106,6 @@ class ProviderRegistry {
return settings.openaiAPIKey != nil && !settings.openaiAPIKey!.isEmpty
case .ollama:
return settings.ollamaConfigured
- case .appleOnDevice:
- return true // no API key needed
}
}
diff --git a/oAI/Utilities/Extensions/Color+Extensions.swift b/oAI/Utilities/Extensions/Color+Extensions.swift
index 6c790a1..e36d750 100644
--- a/oAI/Utilities/Extensions/Color+Extensions.swift
+++ b/oAI/Utilities/Extensions/Color+Extensions.swift
@@ -60,11 +60,10 @@ extension Color {
static func providerColor(_ provider: Settings.Provider) -> Color {
switch provider {
- case .openrouter: return Color(hex: "#7c3aed") // Purple
- case .anthropic: return Color(hex: "#d4895a") // Orange
- case .openai: return Color(hex: "#10a37f") // Green
- case .ollama: return Color(hex: "#ffffff") // White
- case .appleOnDevice: return Color(hex: "#636366") // Apple grey
+ case .openrouter: return Color(hex: "#7c3aed") // Purple
+ case .anthropic: return Color(hex: "#d4895a") // Orange
+ case .openai: return Color(hex: "#10a37f") // Green
+ case .ollama: return Color(hex: "#ffffff") // White
}
}
diff --git a/oAI/ViewModels/ChatViewModel.swift b/oAI/ViewModels/ChatViewModel.swift
index 31695ba..d398f93 100644
--- a/oAI/ViewModels/ChatViewModel.swift
+++ b/oAI/ViewModels/ChatViewModel.swift
@@ -424,8 +424,6 @@ Don't narrate future actions ("Let me...") - just use the tools.
func inferProviderPublic(from modelId: String) -> Settings.Provider? { inferProvider(from: modelId) }
private func inferProvider(from modelId: String) -> Settings.Provider? {
- // Apple Foundation Models
- if modelId.hasPrefix("apple-") { return .appleOnDevice }
// OpenRouter models always contain a "/" (e.g. "anthropic/claude-3-5-sonnet")
if modelId.contains("/") { return .openrouter }
// Anthropic direct (e.g. "claude-sonnet-4-5-20250929")
diff --git a/oAI/Views/Screens/CreditsView.swift b/oAI/Views/Screens/CreditsView.swift
index a3a2298..b60bdd4 100644
--- a/oAI/Views/Screens/CreditsView.swift
+++ b/oAI/Views/Screens/CreditsView.swift
@@ -80,17 +80,6 @@ struct CreditsView: View {
.font(.system(size: 40))
.foregroundColor(.green)
.padding(.top)
-
- case .appleOnDevice:
- Text("Apple Intelligence")
- .font(.headline)
- Text("On-device and free — no credits or API key needed.")
- .font(.body)
- .foregroundColor(.secondary)
- Image(systemName: "apple.logo")
- .font(.system(size: 40))
- .foregroundColor(.secondary)
- .padding(.top)
}
}
diff --git a/oAI/Views/Screens/SettingsView.swift b/oAI/Views/Screens/SettingsView.swift
index 67d4c72..aed4728 100644
--- a/oAI/Views/Screens/SettingsView.swift
+++ b/oAI/Views/Screens/SettingsView.swift
@@ -25,7 +25,6 @@
import SwiftUI
import UniformTypeIdentifiers
-import FoundationModels
struct SettingsView: View {
@Environment(\.dismiss) var dismiss
@@ -307,29 +306,6 @@ It's better to admit "I need more information" or "I cannot do that" than to fak
}
}
- // Apple Intelligence
- VStack(alignment: .leading, spacing: 6) {
- sectionHeader("Apple Intelligence")
- formSection {
- row("Status") {
- appleIntelligenceStatusBadge
- }
- rowDivider()
- row("Model") {
- Text("On-Device (4K context)")
- .foregroundStyle(.secondary)
- }
- rowDivider()
- row("") {
- Button("Open Apple Intelligence Settings") {
- if let url = URL(string: "x-apple.systempreferences:com.apple.preference.aisettings") {
- NSWorkspace.shared.open(url)
- }
- }
- }
- }
- }
-
// Features
VStack(alignment: .leading, spacing: 6) {
sectionHeader("Features")
@@ -2673,28 +2649,6 @@ It's better to admit "I need more information" or "I cannot do that" than to fak
formatter.unitsStyle = .full
return formatter.localizedString(for: date, relativeTo: .now)
}
-
- @ViewBuilder
- private var appleIntelligenceStatusBadge: some View {
- let availability = SystemLanguageModel.default.availability
- switch availability {
- case .available:
- Label("Available", systemImage: "checkmark.circle.fill")
- .foregroundStyle(.green)
- case .unavailable(.deviceNotEligible):
- Label("Not supported on this Mac", systemImage: "xmark.circle.fill")
- .foregroundStyle(.red)
- case .unavailable(.appleIntelligenceNotEnabled):
- Label("Not enabled — open Apple Intelligence Settings", systemImage: "exclamationmark.circle.fill")
- .foregroundStyle(.orange)
- case .unavailable(.modelNotReady):
- Label("Model downloading…", systemImage: "arrow.down.circle.fill")
- .foregroundStyle(.orange)
- default:
- Label("Unavailable", systemImage: "questionmark.circle.fill")
- .foregroundStyle(.secondary)
- }
- }
}
#Preview {