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 <noreply@anthropic.com>
This commit is contained in:
commit
47feeedf9d
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
.build/
|
||||||
|
.swiftpm/
|
||||||
|
*.xcodeproj/
|
||||||
|
xcuserdata/
|
||||||
|
DerivedData/
|
||||||
|
.DS_Store
|
||||||
213
Package.resolved
Normal file
213
Package.resolved
Normal file
@ -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
|
||||||
|
}
|
||||||
31
Package.swift
Normal file
31
Package.swift
Normal file
@ -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"])
|
||||||
|
]
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
27
Sources/AppleIntelligenceGRPC/Config.swift
Normal file
27
Sources/AppleIntelligenceGRPC/Config.swift
Normal file
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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<D: SwiftProtobuf.Decoder>(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<V: SwiftProtobuf.Visitor>(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<D: SwiftProtobuf.Decoder>(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<V: SwiftProtobuf.Visitor>(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<D: SwiftProtobuf.Decoder>(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<V: SwiftProtobuf.Visitor>(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<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
|
||||||
|
while let _ = try decoder.nextFieldNumber() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
func traverse<V: SwiftProtobuf.Visitor>(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<D: SwiftProtobuf.Decoder>(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<V: SwiftProtobuf.Visitor>(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
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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<Transport: ServerTransport>(with router: inout RPCRouter<Transport>) {
|
||||||
|
// Register Complete method (unary)
|
||||||
|
router.registerHandler(
|
||||||
|
forMethod: Methods.complete,
|
||||||
|
deserializer: ProtobufDeserializer<Appleintelligence_CompletionRequest>(),
|
||||||
|
serializer: ProtobufSerializer<Appleintelligence_CompletionResponse>()
|
||||||
|
) { 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<Appleintelligence_CompletionRequest>(),
|
||||||
|
serializer: ProtobufSerializer<Appleintelligence_CompletionChunk>()
|
||||||
|
) { 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<Appleintelligence_HealthRequest>(),
|
||||||
|
serializer: ProtobufSerializer<Appleintelligence_HealthResponse>()
|
||||||
|
) { 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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<String, Error> {
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
61
Sources/AppleIntelligenceGRPC/main.swift
Normal file
61
Sources/AppleIntelligenceGRPC/main.swift
Normal file
@ -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.")
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user