From ced355c116c7603669a57362a85f1024591dc040 Mon Sep 17 00:00:00 2001 From: Mattt Zmuda Date: Tue, 28 Oct 2025 02:29:25 -0700 Subject: [PATCH 1/4] Add Availability type and protocol requirements --- Sources/AnyLanguageModel/Availability.swift | 12 ++++ Sources/AnyLanguageModel/LanguageModel.swift | 18 +++++ .../Models/AnthropicLanguageModel.swift | 4 ++ .../Models/CoreMLLanguageModel.swift | 4 ++ .../Models/LlamaLanguageModel.swift | 4 ++ .../Models/MLXLanguageModel.swift | 4 ++ .../Models/OllamaLanguageModel.swift | 4 ++ .../Models/OpenAILanguageModel.swift | 4 ++ .../Models/SystemLanguageModel.swift | 66 ++++++++++++------- 9 files changed, 95 insertions(+), 25 deletions(-) create mode 100644 Sources/AnyLanguageModel/Availability.swift diff --git a/Sources/AnyLanguageModel/Availability.swift b/Sources/AnyLanguageModel/Availability.swift new file mode 100644 index 00000000..eacaf786 --- /dev/null +++ b/Sources/AnyLanguageModel/Availability.swift @@ -0,0 +1,12 @@ +/// The availability status for a specific language model. +public enum Availability { + /// The model is ready for making requests. + case available + + /// Indicates that the model is not ready for requests. + case unavailable(UnavailableReason) +} + +extension Availability: Equatable where UnavailableReason: Equatable {} +extension Availability: Hashable where UnavailableReason: Hashable {} +extension Availability: Sendable where UnavailableReason: Sendable {} diff --git a/Sources/AnyLanguageModel/LanguageModel.swift b/Sources/AnyLanguageModel/LanguageModel.swift index 947ae67c..e25a7886 100644 --- a/Sources/AnyLanguageModel/LanguageModel.swift +++ b/Sources/AnyLanguageModel/LanguageModel.swift @@ -1,6 +1,10 @@ import Foundation public protocol LanguageModel: Sendable { + associatedtype UnavailableReason + + var availability: Availability { get } + func prewarm( for session: LanguageModelSession, promptPrefix: Prompt? @@ -33,6 +37,14 @@ public protocol LanguageModel: Sendable { // MARK: - Default Implementation extension LanguageModel { + public var isAvailable: Bool { + if case .available = availability { + return true + } else { + return false + } + } + public func prewarm( for session: LanguageModelSession, promptPrefix: Prompt? = nil @@ -49,3 +61,9 @@ extension LanguageModel { return Data() } } + +extension LanguageModel where UnavailableReason == Never { + public var availability: Availability { + return .available + } +} diff --git a/Sources/AnyLanguageModel/Models/AnthropicLanguageModel.swift b/Sources/AnyLanguageModel/Models/AnthropicLanguageModel.swift index bb50699b..d074433b 100644 --- a/Sources/AnyLanguageModel/Models/AnthropicLanguageModel.swift +++ b/Sources/AnyLanguageModel/Models/AnthropicLanguageModel.swift @@ -28,6 +28,10 @@ import OrderedCollections /// ) /// ``` public struct AnthropicLanguageModel: LanguageModel { + /// The reason the model is unavailable. + /// This model is always available. + public typealias UnavailableReason = Never + /// The default base URL for Anthropic's API. public static let defaultBaseURL = URL(string: "https://api.anthropic.com")! diff --git a/Sources/AnyLanguageModel/Models/CoreMLLanguageModel.swift b/Sources/AnyLanguageModel/Models/CoreMLLanguageModel.swift index 852d9f5c..68587e9a 100644 --- a/Sources/AnyLanguageModel/Models/CoreMLLanguageModel.swift +++ b/Sources/AnyLanguageModel/Models/CoreMLLanguageModel.swift @@ -19,6 +19,10 @@ /// ``` @available(macOS 15.0, iOS 18.0, tvOS 18.0, visionOS 2.0, watchOS 11.0, *) public struct CoreMLLanguageModel: AnyLanguageModel.LanguageModel { + /// The reason the model is unavailable. + /// This model is always available. + public typealias UnavailableReason = Never + private let model: Models.LanguageModel private let tokenizer: any Tokenizer private let chatTemplateHandler: (@Sendable (Instructions?, Prompt) -> [Message])? diff --git a/Sources/AnyLanguageModel/Models/LlamaLanguageModel.swift b/Sources/AnyLanguageModel/Models/LlamaLanguageModel.swift index 4650f770..edd3f4a9 100644 --- a/Sources/AnyLanguageModel/Models/LlamaLanguageModel.swift +++ b/Sources/AnyLanguageModel/Models/LlamaLanguageModel.swift @@ -13,6 +13,10 @@ import Foundation /// ) /// ``` public final class LlamaLanguageModel: LanguageModel, @unchecked Sendable { + /// The reason the model is unavailable. + /// This model is always available. + public typealias UnavailableReason = Never + /// The path to the GGUF model file. public let modelPath: String diff --git a/Sources/AnyLanguageModel/Models/MLXLanguageModel.swift b/Sources/AnyLanguageModel/Models/MLXLanguageModel.swift index 83ac2205..2a5e1b3a 100644 --- a/Sources/AnyLanguageModel/Models/MLXLanguageModel.swift +++ b/Sources/AnyLanguageModel/Models/MLXLanguageModel.swift @@ -12,6 +12,10 @@ import Foundation /// let model = MLXLanguageModel(modelId: "mlx-community/Llama-3.2-3B-Instruct-4bit") /// ``` public struct MLXLanguageModel: LanguageModel { + /// The reason the model is unavailable. + /// This model is always available. + public typealias UnavailableReason = Never + /// The model identifier from the MLX community on Hugging Face. public let modelId: String diff --git a/Sources/AnyLanguageModel/Models/OllamaLanguageModel.swift b/Sources/AnyLanguageModel/Models/OllamaLanguageModel.swift index 1dcc205a..f45d1e31 100644 --- a/Sources/AnyLanguageModel/Models/OllamaLanguageModel.swift +++ b/Sources/AnyLanguageModel/Models/OllamaLanguageModel.swift @@ -14,6 +14,10 @@ import OrderedCollections /// let model = OllamaLanguageModel(model: "qwen2.5") /// ``` public struct OllamaLanguageModel: LanguageModel { + /// The reason the model is unavailable. + /// This model is always available. + public typealias UnavailableReason = Never + /// The default base URL for Ollama. public static let defaultBaseURL = URL(string: "http://localhost:11434")! diff --git a/Sources/AnyLanguageModel/Models/OpenAILanguageModel.swift b/Sources/AnyLanguageModel/Models/OpenAILanguageModel.swift index cd2ed1fc..1c2dae1c 100644 --- a/Sources/AnyLanguageModel/Models/OpenAILanguageModel.swift +++ b/Sources/AnyLanguageModel/Models/OpenAILanguageModel.swift @@ -17,6 +17,10 @@ import JSONSchema /// ) /// ``` public struct OpenAILanguageModel: LanguageModel { + /// The reason the model is unavailable. + /// This model is always available. + public typealias UnavailableReason = Never + /// The default base URL for OpenAI's API. public static let defaultBaseURL = URL(string: "https://api.openai.com/v1/")! diff --git a/Sources/AnyLanguageModel/Models/SystemLanguageModel.swift b/Sources/AnyLanguageModel/Models/SystemLanguageModel.swift index 3c4b641b..0f66f519 100644 --- a/Sources/AnyLanguageModel/Models/SystemLanguageModel.swift +++ b/Sources/AnyLanguageModel/Models/SystemLanguageModel.swift @@ -12,9 +12,17 @@ /// let model = SystemLanguageModel() /// ``` @available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *) - public struct SystemLanguageModel { + public struct SystemLanguageModel: LanguageModel { + /// The reason the model is unavailable. + public typealias UnavailableReason = FoundationModels.SystemLanguageModel.Availability.UnavailableReason + let systemModel: FoundationModels.SystemLanguageModel + /// The default system language model. + public static var `default`: SystemLanguageModel { + SystemLanguageModel() + } + /// Creates the default system language model. public init() { self.systemModel = FoundationModels.SystemLanguageModel.default @@ -44,31 +52,15 @@ ) { self.systemModel = FoundationModels.SystemLanguageModel(adapter: adapter, guardrails: guardrails) } - } - - @available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *) - extension SystemLanguageModel: LanguageModel { - public func logFeedbackAttachment( - within session: LanguageModelSession, - sentiment: LanguageModelFeedback.Sentiment?, - issues: [LanguageModelFeedback.Issue], - desiredOutput: Transcript.Entry? - ) -> Data { - let fmSession = FoundationModels.LanguageModelSession( - model: systemModel, - tools: session.tools.toFoundationModels(), - instructions: session.instructions?.toFoundationModels() - ) - let fmSentiment = sentiment?.toFoundationModels() - let fmIssues = issues.map { $0.toFoundationModels() } - let fmDesiredOutput: FoundationModels.Transcript.Entry? = nil - - return fmSession.logFeedbackAttachment( - sentiment: fmSentiment, - issues: fmIssues, - desiredOutput: fmDesiredOutput - ) + /// The availability status for the system language model. + public var availability: Availability { + switch systemModel.availability { + case .available: + .available + case .unavailable(let reason): + .unavailable(reason) + } } public func respond( @@ -187,6 +179,30 @@ return LanguageModelSession.ResponseStream(stream: stream) } + + public func logFeedbackAttachment( + within session: LanguageModelSession, + sentiment: LanguageModelFeedback.Sentiment?, + issues: [LanguageModelFeedback.Issue], + desiredOutput: Transcript.Entry? + ) -> Data { + let fmSession = FoundationModels.LanguageModelSession( + model: systemModel, + tools: session.tools.toFoundationModels(), + instructions: session.instructions?.toFoundationModels() + ) + + let fmSentiment = sentiment?.toFoundationModels() + let fmIssues = issues.map { $0.toFoundationModels() } + let fmDesiredOutput: FoundationModels.Transcript.Entry? = nil + + return fmSession.logFeedbackAttachment( + sentiment: fmSentiment, + issues: fmIssues, + desiredOutput: fmDesiredOutput + ) + } + } // MARK: - Helpers From 5e5e66c94e94db0a7d54d33852c2ec0127330f3b Mon Sep 17 00:00:00 2001 From: Mattt Zmuda Date: Tue, 28 Oct 2025 03:03:12 -0700 Subject: [PATCH 2/4] Update MockLanguageModel --- .../MockLanguageModelTests.swift | 11 +++++++++++ .../Shared/MockLanguageModel.swift | 18 +++++++++++++++++- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/Tests/AnyLanguageModelTests/MockLanguageModelTests.swift b/Tests/AnyLanguageModelTests/MockLanguageModelTests.swift index ef881b9d..416670a3 100644 --- a/Tests/AnyLanguageModelTests/MockLanguageModelTests.swift +++ b/Tests/AnyLanguageModelTests/MockLanguageModelTests.swift @@ -48,4 +48,15 @@ struct MockLanguageModelTests { #expect(response.content == expected) } } + + @Test func unavailable() async throws { + let model = MockLanguageModel.unavailable + let session = LanguageModelSession(model: model) + + #expect(model.availability == .unavailable(.custom("MockLanguageModel is unavailable"))) + #expect(model.isAvailable == false) + + let response = try await session.respond(to: "Say hello") + #expect(response.content == "MockLanguageModel is unavailable") + } } diff --git a/Tests/AnyLanguageModelTests/Shared/MockLanguageModel.swift b/Tests/AnyLanguageModelTests/Shared/MockLanguageModel.swift index c9e9e2c0..edd326da 100644 --- a/Tests/AnyLanguageModelTests/Shared/MockLanguageModel.swift +++ b/Tests/AnyLanguageModelTests/Shared/MockLanguageModel.swift @@ -1,16 +1,26 @@ @testable import AnyLanguageModel struct MockLanguageModel: LanguageModel { + enum UnavailableReason: Hashable, Sendable { + case custom(String) + } + + var availabilityProvider: @Sendable () -> Availability var responseProvider: @Sendable (Prompt, GenerationOptions) async throws -> String init( - responseProvider: + _ responseProvider: @escaping @Sendable (Prompt, GenerationOptions) async throws -> String = { _, _ in "Mock response" } ) { + self.availabilityProvider = { .available } self.responseProvider = responseProvider } + var availability: Availability { + return availabilityProvider() + } + func respond( within session: LanguageModelSession, to prompt: Prompt, @@ -68,4 +78,10 @@ extension MockLanguageModel { static func fixed(_ response: String) -> Self { MockLanguageModel { _, _ in response } } + + static var unavailable: Self { + var model = MockLanguageModel() + model.availabilityProvider = { .unavailable(.custom("MockLanguageModel is unavailable")) } + return model + } } From 1084115a4ad2222c1afabb3dc2ff8565f5f0cbae Mon Sep 17 00:00:00 2001 From: Mattt Zmuda Date: Tue, 28 Oct 2025 03:03:36 -0700 Subject: [PATCH 3/4] Use public isAvailable method in test, change to non-@testable import --- Tests/AnyLanguageModelTests/SystemLanguageModelTests.swift | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Tests/AnyLanguageModelTests/SystemLanguageModelTests.swift b/Tests/AnyLanguageModelTests/SystemLanguageModelTests.swift index 759f6e4e..fab5ac25 100644 --- a/Tests/AnyLanguageModelTests/SystemLanguageModelTests.swift +++ b/Tests/AnyLanguageModelTests/SystemLanguageModelTests.swift @@ -1,11 +1,10 @@ import Testing - -@testable import AnyLanguageModel +import AnyLanguageModel #if canImport(FoundationModels) private let isSystemLanguageModelAvailable = { if #available(macOS 26.0, *) { - return SystemLanguageModel().systemModel.isAvailable + return SystemLanguageModel.default.isAvailable } else { return false } From 8ff97266109309ec90ebffb3c9ce34b52a556411 Mon Sep 17 00:00:00 2001 From: Mattt Zmuda Date: Tue, 28 Oct 2025 03:16:30 -0700 Subject: [PATCH 4/4] Fix MockLanguageModel tests --- Tests/AnyLanguageModelTests/MockLanguageModelTests.swift | 4 ---- Tests/AnyLanguageModelTests/Shared/MockLanguageModel.swift | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/Tests/AnyLanguageModelTests/MockLanguageModelTests.swift b/Tests/AnyLanguageModelTests/MockLanguageModelTests.swift index 416670a3..3a8f4db5 100644 --- a/Tests/AnyLanguageModelTests/MockLanguageModelTests.swift +++ b/Tests/AnyLanguageModelTests/MockLanguageModelTests.swift @@ -51,12 +51,8 @@ struct MockLanguageModelTests { @Test func unavailable() async throws { let model = MockLanguageModel.unavailable - let session = LanguageModelSession(model: model) #expect(model.availability == .unavailable(.custom("MockLanguageModel is unavailable"))) #expect(model.isAvailable == false) - - let response = try await session.respond(to: "Say hello") - #expect(response.content == "MockLanguageModel is unavailable") } } diff --git a/Tests/AnyLanguageModelTests/Shared/MockLanguageModel.swift b/Tests/AnyLanguageModelTests/Shared/MockLanguageModel.swift index edd326da..8f65b212 100644 --- a/Tests/AnyLanguageModelTests/Shared/MockLanguageModel.swift +++ b/Tests/AnyLanguageModelTests/Shared/MockLanguageModel.swift @@ -80,7 +80,7 @@ extension MockLanguageModel { } static var unavailable: Self { - var model = MockLanguageModel() + var model = MockLanguageModel.echo model.availabilityProvider = { .unavailable(.custom("MockLanguageModel is unavailable")) } return model }