commit 47feeedf9dc2bf9d2a624731a82cd44a2d23b3cd Author: Mathias Beaulieu-Duncan Date: Tue Dec 30 02:54:12 2025 -0500 Add Apple Intelligence gRPC server Implements a Swift gRPC server that exposes Apple's Foundation Models (Apple Intelligence) over the network for LAN access. Features: - Complete: Unary RPC for prompt/response - StreamComplete: Server streaming RPC for token-by-token responses - Health: Check model availability - Optional API key authentication via gRPC metadata - Configurable host/port via CLI args or environment variables Requires macOS 26 (Tahoe) with Apple Intelligence enabled. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b7c6dd7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.build/ +.swiftpm/ +*.xcodeproj/ +xcuserdata/ +DerivedData/ +.DS_Store diff --git a/Package.resolved b/Package.resolved new file mode 100644 index 0000000..d54996e --- /dev/null +++ b/Package.resolved @@ -0,0 +1,213 @@ +{ + "originHash" : "73128af91f020c013de06bf6af5d06131ff05e38285118f5ff904ee06a3a6e24", + "pins" : [ + { + "identity" : "grpc-swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/grpc/grpc-swift.git", + "state" : { + "revision" : "adc18c3e1c55027d0ce43893897ac448e3f27ebe", + "version" : "2.2.3" + } + }, + { + "identity" : "grpc-swift-nio-transport", + "kind" : "remoteSourceControl", + "location" : "https://github.com/grpc/grpc-swift-nio-transport.git", + "state" : { + "revision" : "ca2303eb7f3df556beafbba33a143ffa30d5b786", + "version" : "1.2.3" + } + }, + { + "identity" : "grpc-swift-protobuf", + "kind" : "remoteSourceControl", + "location" : "https://github.com/grpc/grpc-swift-protobuf.git", + "state" : { + "revision" : "53e89e3a5d417307f70a721c7b83e564fefb1e1c", + "version" : "1.3.1" + } + }, + { + "identity" : "swift-algorithms", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-algorithms.git", + "state" : { + "revision" : "87e50f483c54e6efd60e885f7f5aa946cee68023", + "version" : "1.2.1" + } + }, + { + "identity" : "swift-argument-parser", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-argument-parser.git", + "state" : { + "revision" : "c5d11a805e765f52ba34ec7284bd4fcd6ba68615", + "version" : "1.7.0" + } + }, + { + "identity" : "swift-asn1", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-asn1.git", + "state" : { + "revision" : "810496cf121e525d660cd0ea89a758740476b85f", + "version" : "1.5.1" + } + }, + { + "identity" : "swift-async-algorithms", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-async-algorithms.git", + "state" : { + "revision" : "6c050d5ef8e1aa6342528460db614e9770d7f804", + "version" : "1.1.1" + } + }, + { + "identity" : "swift-atomics", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-atomics.git", + "state" : { + "revision" : "b601256eab081c0f92f059e12818ac1d4f178ff7", + "version" : "1.3.0" + } + }, + { + "identity" : "swift-certificates", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-certificates.git", + "state" : { + "revision" : "133a347911b6ad0fc8fe3bf46ca90c66cff97130", + "version" : "1.17.0" + } + }, + { + "identity" : "swift-collections", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-collections.git", + "state" : { + "revision" : "7b847a3b7008b2dc2f47ca3110d8c782fb2e5c7e", + "version" : "1.3.0" + } + }, + { + "identity" : "swift-crypto", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-crypto.git", + "state" : { + "revision" : "6f70fa9eab24c1fd982af18c281c4525d05e3095", + "version" : "4.2.0" + } + }, + { + "identity" : "swift-http-structured-headers", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-http-structured-headers.git", + "state" : { + "revision" : "76d7627bd88b47bf5a0f8497dd244885960dde0b", + "version" : "1.6.0" + } + }, + { + "identity" : "swift-http-types", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-http-types.git", + "state" : { + "revision" : "45eb0224913ea070ec4fba17291b9e7ecf4749ca", + "version" : "1.5.1" + } + }, + { + "identity" : "swift-log", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-log.git", + "state" : { + "revision" : "bc386b95f2a16ccd0150a8235e7c69eab2b866ca", + "version" : "1.8.0" + } + }, + { + "identity" : "swift-nio", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio.git", + "state" : { + "revision" : "a1605a3303a28e14d822dec8aaa53da8a9490461", + "version" : "2.92.0" + } + }, + { + "identity" : "swift-nio-extras", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio-extras.git", + "state" : { + "revision" : "1c90641b02b6ab47c6d0db2063a12198b04e83e2", + "version" : "1.31.2" + } + }, + { + "identity" : "swift-nio-http2", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio-http2.git", + "state" : { + "revision" : "c2ba4cfbb83f307c66f5a6df6bb43e3c88dfbf80", + "version" : "1.39.0" + } + }, + { + "identity" : "swift-nio-ssl", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio-ssl.git", + "state" : { + "revision" : "173cc69a058623525a58ae6710e2f5727c663793", + "version" : "2.36.0" + } + }, + { + "identity" : "swift-nio-transport-services", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio-transport-services.git", + "state" : { + "revision" : "60c3e187154421171721c1a38e800b390680fb5d", + "version" : "1.26.0" + } + }, + { + "identity" : "swift-numerics", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-numerics.git", + "state" : { + "revision" : "0c0290ff6b24942dadb83a929ffaaa1481df04a2", + "version" : "1.1.1" + } + }, + { + "identity" : "swift-protobuf", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-protobuf.git", + "state" : { + "revision" : "c169a5744230951031770e27e475ff6eefe51f9d", + "version" : "1.33.3" + } + }, + { + "identity" : "swift-service-lifecycle", + "kind" : "remoteSourceControl", + "location" : "https://github.com/swift-server/swift-service-lifecycle.git", + "state" : { + "revision" : "1de37290c0ab3c5a96028e0f02911b672fd42348", + "version" : "2.9.1" + } + }, + { + "identity" : "swift-system", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-system.git", + "state" : { + "revision" : "395a77f0aa927f0ff73941d7ac35f2b46d47c9db", + "version" : "1.6.3" + } + } + ], + "version" : 3 +} diff --git a/Package.swift b/Package.swift new file mode 100644 index 0000000..a54a0cd --- /dev/null +++ b/Package.swift @@ -0,0 +1,31 @@ +// swift-tools-version: 6.2 +import PackageDescription + +let package = Package( + name: "AppleIntelligenceGRPC", + platforms: [ + .macOS(.v26) + ], + dependencies: [ + .package(url: "https://github.com/grpc/grpc-swift.git", from: "2.0.0"), + .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", from: "1.0.0"), + .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", from: "1.0.0"), + .package(url: "https://github.com/apple/swift-protobuf.git", from: "1.28.0"), + .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.3.0"), + ], + targets: [ + .executableTarget( + name: "AppleIntelligenceGRPC", + dependencies: [ + .product(name: "GRPCCore", package: "grpc-swift"), + .product(name: "GRPCNIOTransportHTTP2", package: "grpc-swift-nio-transport"), + .product(name: "GRPCProtobuf", package: "grpc-swift-protobuf"), + .product(name: "SwiftProtobuf", package: "swift-protobuf"), + .product(name: "ArgumentParser", package: "swift-argument-parser"), + ], + swiftSettings: [ + .unsafeFlags(["-parse-as-library"]) + ] + ), + ] +) diff --git a/Sources/AppleIntelligenceGRPC/Config.swift b/Sources/AppleIntelligenceGRPC/Config.swift new file mode 100644 index 0000000..560ec5b --- /dev/null +++ b/Sources/AppleIntelligenceGRPC/Config.swift @@ -0,0 +1,27 @@ +import Foundation + +/// Server configuration loaded from environment variables +struct Config { + /// Host to bind the server to (default: 0.0.0.0 for LAN access) + let host: String + + /// Port to listen on (default: 50051) + let port: Int + + /// Optional API key for authentication via gRPC metadata + let apiKey: String? + + /// Initialize configuration from environment variables + init() { + self.host = ProcessInfo.processInfo.environment["GRPC_HOST"] ?? "0.0.0.0" + self.port = Int(ProcessInfo.processInfo.environment["GRPC_PORT"] ?? "50051") ?? 50051 + self.apiKey = ProcessInfo.processInfo.environment["API_KEY"] + } + + /// Initialize with explicit values (for testing) + init(host: String, port: Int, apiKey: String? = nil) { + self.host = host + self.port = port + self.apiKey = apiKey + } +} diff --git a/Sources/AppleIntelligenceGRPC/Generated/AppleIntelligence.pb.swift b/Sources/AppleIntelligenceGRPC/Generated/AppleIntelligence.pb.swift new file mode 100644 index 0000000..9956962 --- /dev/null +++ b/Sources/AppleIntelligenceGRPC/Generated/AppleIntelligence.pb.swift @@ -0,0 +1,238 @@ +// DO NOT EDIT. +// swift-format-ignore-file +// swiftlint:disable all +// +// Generated protocol buffer code for apple_intelligence.proto + +import Foundation +import SwiftProtobuf + +// MARK: - Messages + +struct Appleintelligence_CompletionRequest: Sendable, SwiftProtobuf.Message { + static let protoMessageName: String = "appleintelligence.CompletionRequest" + + var prompt: String = "" + var temperature: Float = 0 + var maxTokens: Int32 = 0 + + var hasTemperature: Bool = false + var hasMaxTokens: Bool = false + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} + + init(prompt: String, temperature: Float? = nil, maxTokens: Int32? = nil) { + self.prompt = prompt + if let temp = temperature { + self.temperature = temp + self.hasTemperature = true + } + if let tokens = maxTokens { + self.maxTokens = tokens + self.hasMaxTokens = true + } + } + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + switch fieldNumber { + case 1: try decoder.decodeSingularStringField(value: &prompt) + case 2: + try decoder.decodeSingularFloatField(value: &temperature) + hasTemperature = true + case 3: + try decoder.decodeSingularInt32Field(value: &maxTokens) + hasMaxTokens = true + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if !prompt.isEmpty { + try visitor.visitSingularStringField(value: prompt, fieldNumber: 1) + } + if hasTemperature { + try visitor.visitSingularFloatField(value: temperature, fieldNumber: 2) + } + if hasMaxTokens { + try visitor.visitSingularInt32Field(value: maxTokens, fieldNumber: 3) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Self, rhs: Self) -> Bool { + lhs.prompt == rhs.prompt && lhs.temperature == rhs.temperature && lhs.maxTokens == rhs.maxTokens && lhs.unknownFields == rhs.unknownFields + } + + func isEqualTo(message: any SwiftProtobuf.Message) -> Bool { + guard let other = message as? Self else { return false } + return self == other + } +} + +struct Appleintelligence_CompletionResponse: Sendable, SwiftProtobuf.Message { + static let protoMessageName: String = "appleintelligence.CompletionResponse" + + var id: String = "" + var text: String = "" + var finishReason: String = "" + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + switch fieldNumber { + case 1: try decoder.decodeSingularStringField(value: &id) + case 2: try decoder.decodeSingularStringField(value: &text) + case 3: try decoder.decodeSingularStringField(value: &finishReason) + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if !id.isEmpty { + try visitor.visitSingularStringField(value: id, fieldNumber: 1) + } + if !text.isEmpty { + try visitor.visitSingularStringField(value: text, fieldNumber: 2) + } + if !finishReason.isEmpty { + try visitor.visitSingularStringField(value: finishReason, fieldNumber: 3) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Self, rhs: Self) -> Bool { + lhs.id == rhs.id && lhs.text == rhs.text && lhs.finishReason == rhs.finishReason && lhs.unknownFields == rhs.unknownFields + } + + func isEqualTo(message: any SwiftProtobuf.Message) -> Bool { + guard let other = message as? Self else { return false } + return self == other + } +} + +struct Appleintelligence_CompletionChunk: Sendable, SwiftProtobuf.Message { + static let protoMessageName: String = "appleintelligence.CompletionChunk" + + var id: String = "" + var delta: String = "" + var isFinal: Bool = false + var finishReason: String = "" + + var hasFinishReason: Bool { + !finishReason.isEmpty + } + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + switch fieldNumber { + case 1: try decoder.decodeSingularStringField(value: &id) + case 2: try decoder.decodeSingularStringField(value: &delta) + case 3: try decoder.decodeSingularBoolField(value: &isFinal) + case 4: try decoder.decodeSingularStringField(value: &finishReason) + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if !id.isEmpty { + try visitor.visitSingularStringField(value: id, fieldNumber: 1) + } + if !delta.isEmpty { + try visitor.visitSingularStringField(value: delta, fieldNumber: 2) + } + if isFinal { + try visitor.visitSingularBoolField(value: isFinal, fieldNumber: 3) + } + if !finishReason.isEmpty { + try visitor.visitSingularStringField(value: finishReason, fieldNumber: 4) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Self, rhs: Self) -> Bool { + lhs.id == rhs.id && lhs.delta == rhs.delta && lhs.isFinal == rhs.isFinal && lhs.finishReason == rhs.finishReason && lhs.unknownFields == rhs.unknownFields + } + + func isEqualTo(message: any SwiftProtobuf.Message) -> Bool { + guard let other = message as? Self else { return false } + return self == other + } +} + +struct Appleintelligence_HealthRequest: Sendable, SwiftProtobuf.Message { + static let protoMessageName: String = "appleintelligence.HealthRequest" + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} + + mutating func decodeMessage(decoder: inout D) throws { + while let _ = try decoder.nextFieldNumber() {} + } + + func traverse(visitor: inout V) throws { + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Self, rhs: Self) -> Bool { + lhs.unknownFields == rhs.unknownFields + } + + func isEqualTo(message: any SwiftProtobuf.Message) -> Bool { + guard let other = message as? Self else { return false } + return self == other + } +} + +struct Appleintelligence_HealthResponse: Sendable, SwiftProtobuf.Message { + static let protoMessageName: String = "appleintelligence.HealthResponse" + + var healthy: Bool = false + var modelStatus: String = "" + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + switch fieldNumber { + case 1: try decoder.decodeSingularBoolField(value: &healthy) + case 2: try decoder.decodeSingularStringField(value: &modelStatus) + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if healthy { + try visitor.visitSingularBoolField(value: healthy, fieldNumber: 1) + } + if !modelStatus.isEmpty { + try visitor.visitSingularStringField(value: modelStatus, fieldNumber: 2) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Self, rhs: Self) -> Bool { + lhs.healthy == rhs.healthy && lhs.modelStatus == rhs.modelStatus && lhs.unknownFields == rhs.unknownFields + } + + func isEqualTo(message: any SwiftProtobuf.Message) -> Bool { + guard let other = message as? Self else { return false } + return self == other + } +} diff --git a/Sources/AppleIntelligenceGRPC/Providers/AppleIntelligenceProvider.swift b/Sources/AppleIntelligenceGRPC/Providers/AppleIntelligenceProvider.swift new file mode 100644 index 0000000..adb50b2 --- /dev/null +++ b/Sources/AppleIntelligenceGRPC/Providers/AppleIntelligenceProvider.swift @@ -0,0 +1,176 @@ +import Foundation +import GRPCCore +import GRPCProtobuf +import GRPCNIOTransportHTTP2 + +/// gRPC service provider for Apple Intelligence +struct AppleIntelligenceProvider: RegistrableRPCService { + /// Service descriptor + static let serviceDescriptor = ServiceDescriptor( + fullyQualifiedService: "appleintelligence.AppleIntelligence" + ) + + /// Method descriptors + enum Methods { + static let complete = MethodDescriptor( + service: AppleIntelligenceProvider.serviceDescriptor, + method: "Complete" + ) + static let streamComplete = MethodDescriptor( + service: AppleIntelligenceProvider.serviceDescriptor, + method: "StreamComplete" + ) + static let health = MethodDescriptor( + service: AppleIntelligenceProvider.serviceDescriptor, + method: "Health" + ) + } + + /// The underlying AI service + private let service: AppleIntelligenceService + + /// Optional API key for authentication + private let apiKey: String? + + init(service: AppleIntelligenceService, apiKey: String? = nil) { + self.service = service + self.apiKey = apiKey + } + + func registerMethods(with router: inout RPCRouter) { + // Register Complete method (unary) + router.registerHandler( + forMethod: Methods.complete, + deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer() + ) { request, context in + try self.validateApiKey(metadata: request.metadata) + + // Collect the single message from the request stream + var requestMessage: Appleintelligence_CompletionRequest? + for try await message in request.messages { + requestMessage = message + break + } + + guard let message = requestMessage else { + throw RPCError(code: .invalidArgument, message: "No request message received") + } + + let text = try await self.service.complete( + prompt: message.prompt, + temperature: message.hasTemperature ? message.temperature : nil, + maxTokens: message.hasMaxTokens ? Int(message.maxTokens) : nil + ) + + var response = Appleintelligence_CompletionResponse() + response.id = UUID().uuidString + response.text = text + response.finishReason = "stop" + + return StreamingServerResponse(single: ServerResponse(message: response)) + } + + // Register StreamComplete method (server streaming) + router.registerHandler( + forMethod: Methods.streamComplete, + deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer() + ) { request, context in + try self.validateApiKey(metadata: request.metadata) + + // Collect the single message from the request stream + var requestMessage: Appleintelligence_CompletionRequest? + for try await message in request.messages { + requestMessage = message + break + } + + guard let message = requestMessage else { + throw RPCError(code: .invalidArgument, message: "No request message received") + } + + let completionId = UUID().uuidString + let prompt = message.prompt + let temperature = message.hasTemperature ? message.temperature : nil + let maxTokens = message.hasMaxTokens ? Int(message.maxTokens) : nil + + return StreamingServerResponse { writer in + let stream = await self.service.streamComplete( + prompt: prompt, + temperature: temperature, + maxTokens: maxTokens + ) + + var lastContent = "" + for try await partialResponse in stream { + // Calculate the delta (new text since last response) + let delta: String + if partialResponse.hasPrefix(lastContent) { + delta = String(partialResponse.dropFirst(lastContent.count)) + } else { + delta = partialResponse + } + lastContent = partialResponse + + if !delta.isEmpty { + var chunk = Appleintelligence_CompletionChunk() + chunk.id = completionId + chunk.delta = delta + chunk.isFinal = false + try await writer.write(chunk) + } + } + + // Send final chunk + var finalChunk = Appleintelligence_CompletionChunk() + finalChunk.id = completionId + finalChunk.delta = "" + finalChunk.isFinal = true + finalChunk.finishReason = "stop" + try await writer.write(finalChunk) + + return [:] + } + } + + // Register Health method (unary) + router.registerHandler( + forMethod: Methods.health, + deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer() + ) { request, context in + // Consume request messages (empty for health check) + for try await _ in request.messages {} + + let isHealthy = await self.service.isAvailable + let modelStatus = await self.service.getModelStatus() + + var response = Appleintelligence_HealthResponse() + response.healthy = isHealthy + response.modelStatus = modelStatus + + return StreamingServerResponse(single: ServerResponse(message: response)) + } + } + + /// Validate API key if configured + private func validateApiKey(metadata: Metadata) throws { + guard let expectedKey = apiKey else { + return // No API key required + } + + // Look for Authorization header in metadata + let authValues = metadata["authorization"] + guard let authHeader = authValues.first(where: { _ in true }), + case .string(let authString) = authHeader, + authString.hasPrefix("Bearer ") else { + throw RPCError(code: .unauthenticated, message: "Missing or invalid Authorization header") + } + + let providedKey = String(authString.dropFirst("Bearer ".count)) + guard providedKey == expectedKey else { + throw RPCError(code: .unauthenticated, message: "Invalid API key") + } + } +} diff --git a/Sources/AppleIntelligenceGRPC/Services/AppleIntelligenceService.swift b/Sources/AppleIntelligenceGRPC/Services/AppleIntelligenceService.swift new file mode 100644 index 0000000..46186ad --- /dev/null +++ b/Sources/AppleIntelligenceGRPC/Services/AppleIntelligenceService.swift @@ -0,0 +1,97 @@ +import Foundation +import FoundationModels + +/// Errors that can occur when using Apple Intelligence +enum AppleIntelligenceError: Error, CustomStringConvertible { + case modelNotAvailable + case generationFailed(String) + case sessionCreationFailed + + var description: String { + switch self { + case .modelNotAvailable: + return "Apple Intelligence model is not available on this device" + case .generationFailed(let reason): + return "Generation failed: \(reason)" + case .sessionCreationFailed: + return "Failed to create language model session" + } + } +} + +/// Service wrapper for Apple Intelligence Foundation Models +actor AppleIntelligenceService { + /// The language model session + private var session: LanguageModelSession? + + /// Whether the model is available + private(set) var isAvailable: Bool = false + + /// Initialize and check model availability + init() async { + await checkAvailability() + } + + /// Check if Apple Intelligence is available + private func checkAvailability() async { + let availability = SystemLanguageModel.default.availability + switch availability { + case .available: + isAvailable = true + session = LanguageModelSession() + case .unavailable: + isAvailable = false + @unknown default: + isAvailable = false + } + } + + /// Get the current model status as a string + func getModelStatus() -> String { + let availability = SystemLanguageModel.default.availability + switch availability { + case .available: + return "available" + case .unavailable(let reason): + return "unavailable: \(reason)" + @unknown default: + return "unknown" + } + } + + /// Generate a completion for the given prompt (non-streaming) + func complete(prompt: String, temperature: Float?, maxTokens: Int?) async throws -> String { + guard isAvailable, let session = session else { + throw AppleIntelligenceError.modelNotAvailable + } + + let response = try await session.respond(to: prompt) + return response.content + } + + /// Generate a streaming completion for the given prompt + func streamComplete( + prompt: String, + temperature: Float?, + maxTokens: Int? + ) -> AsyncThrowingStream { + AsyncThrowingStream { continuation in + Task { + guard self.isAvailable, let session = self.session else { + continuation.finish(throwing: AppleIntelligenceError.modelNotAvailable) + return + } + + do { + let stream = session.streamResponse(to: prompt) + for try await partialResponse in stream { + continuation.yield(partialResponse.content) + } + continuation.finish() + } catch { + continuation.finish(throwing: AppleIntelligenceError.generationFailed(error.localizedDescription)) + } + } + } + } +} diff --git a/Sources/AppleIntelligenceGRPC/main.swift b/Sources/AppleIntelligenceGRPC/main.swift new file mode 100644 index 0000000..0b5ae22 --- /dev/null +++ b/Sources/AppleIntelligenceGRPC/main.swift @@ -0,0 +1,61 @@ +import Foundation +import GRPCCore +import GRPCNIOTransportHTTP2 +import ArgumentParser + +@main +struct AppleIntelligenceServer: AsyncParsableCommand { + static let configuration = CommandConfiguration( + abstract: "Apple Intelligence gRPC Server", + discussion: "Exposes Apple Intelligence (Foundation Models) over gRPC for LAN access." + ) + + @Option(name: .shortAndLong, help: "Host to bind to") + var host: String? + + @Option(name: .shortAndLong, help: "Port to listen on") + var port: Int? + + func run() async throws { + let config = Config() + let bindHost = host ?? config.host + let bindPort = port ?? config.port + + print("Initializing Apple Intelligence service...") + let service = await AppleIntelligenceService() + + let modelStatus = await service.getModelStatus() + print("Model status: \(modelStatus)") + + guard await service.isAvailable else { + print("Error: Apple Intelligence is not available on this device.") + print("Please ensure:") + print(" - You are running macOS 26 (Tahoe) or later") + print(" - You have an Apple Silicon Mac") + print(" - Apple Intelligence is enabled in System Settings") + throw ExitCode.failure + } + + let provider = AppleIntelligenceProvider(service: service, apiKey: config.apiKey) + + let transport = HTTP2ServerTransport.Posix( + address: .ipv4(host: bindHost, port: bindPort), + transportSecurity: .plaintext, + config: .defaults + ) + + let server = GRPCServer(transport: transport, services: [provider]) + + print("Starting gRPC server on \(bindHost):\(bindPort)...") + if config.apiKey != nil { + print("API key authentication is enabled") + } + print("Server is ready to accept connections") + print("Health check: grpcurl -plaintext \(bindHost):\(bindPort) appleintelligence.AppleIntelligence/Health") + print("Press Ctrl+C to stop the server") + + try await server.serve() + + print("Server stopped.") + } +}