Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions Sources/AnyLanguageModel/Availability.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/// The availability status for a specific language model.
public enum Availability<UnavailableReason> {
/// 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 {}
18 changes: 18 additions & 0 deletions Sources/AnyLanguageModel/LanguageModel.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import Foundation

public protocol LanguageModel: Sendable {
associatedtype UnavailableReason

var availability: Availability<UnavailableReason> { get }

func prewarm(
for session: LanguageModelSession,
promptPrefix: Prompt?
Expand Down Expand Up @@ -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
Expand All @@ -49,3 +61,9 @@ extension LanguageModel {
return Data()
}
}

extension LanguageModel where UnavailableReason == Never {
public var availability: Availability<UnavailableReason> {
return .available
}
}
4 changes: 4 additions & 0 deletions Sources/AnyLanguageModel/Models/AnthropicLanguageModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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")!

Expand Down
4 changes: 4 additions & 0 deletions Sources/AnyLanguageModel/Models/CoreMLLanguageModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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])?
Expand Down
4 changes: 4 additions & 0 deletions Sources/AnyLanguageModel/Models/LlamaLanguageModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 4 additions & 0 deletions Sources/AnyLanguageModel/Models/MLXLanguageModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 4 additions & 0 deletions Sources/AnyLanguageModel/Models/OllamaLanguageModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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")!

Expand Down
4 changes: 4 additions & 0 deletions Sources/AnyLanguageModel/Models/OpenAILanguageModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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/")!

Expand Down
66 changes: 41 additions & 25 deletions Sources/AnyLanguageModel/Models/SystemLanguageModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<UnavailableReason> {
switch systemModel.availability {
case .available:
.available
case .unavailable(let reason):
.unavailable(reason)
}
}

public func respond<Content>(
Expand Down Expand Up @@ -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
Expand Down
11 changes: 11 additions & 0 deletions Tests/AnyLanguageModelTests/MockLanguageModelTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
}
18 changes: 17 additions & 1 deletion Tests/AnyLanguageModelTests/Shared/MockLanguageModel.swift
Original file line number Diff line number Diff line change
@@ -1,16 +1,26 @@
@testable import AnyLanguageModel

struct MockLanguageModel: LanguageModel {
enum UnavailableReason: Hashable, Sendable {
case custom(String)
}

var availabilityProvider: @Sendable () -> Availability<UnavailableReason>
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<UnavailableReason> {
return availabilityProvider()
}

func respond<Content>(
within session: LanguageModelSession,
to prompt: Prompt,
Expand Down Expand Up @@ -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
}
}
5 changes: 2 additions & 3 deletions Tests/AnyLanguageModelTests/SystemLanguageModelTests.swift
Original file line number Diff line number Diff line change
@@ -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
}
Expand Down
Loading