From ebe5270c7e1271028360a90c559e9f10c73b6f95 Mon Sep 17 00:00:00 2001 From: Philipp Date: Wed, 5 Mar 2025 19:19:12 +0100 Subject: [PATCH 1/6] Remove dependency on Combine on systems which do not have Combine. --- Sources/OllamaKit/OllamaKit+Chat.swift | 9 +++++++-- Sources/OllamaKit/OllamaKit+CopyModel.swift | 9 +++++++-- Sources/OllamaKit/OllamaKit+DeleteModel.swift | 9 +++++++-- Sources/OllamaKit/OllamaKit+Embeddings.swift | 9 +++++++-- Sources/OllamaKit/OllamaKit+Generate.swift | 9 +++++++-- Sources/OllamaKit/OllamaKit+ModelInfo.swift | 9 +++++++-- Sources/OllamaKit/OllamaKit+Models.swift | 9 +++++++-- Sources/OllamaKit/OllamaKit+PullModel.swift | 8 ++++++-- Sources/OllamaKit/OllamaKit+Reachable.swift | 9 +++++++-- Sources/OllamaKit/Utils/OKHTTPClient.swift | 5 ++++- Sources/OllamaKit/Utils/StreamingDelegate.swift | 2 ++ 11 files changed, 68 insertions(+), 19 deletions(-) diff --git a/Sources/OllamaKit/OllamaKit+Chat.swift b/Sources/OllamaKit/OllamaKit+Chat.swift index 71dd0bd..be97e68 100644 --- a/Sources/OllamaKit/OllamaKit+Chat.swift +++ b/Sources/OllamaKit/OllamaKit+Chat.swift @@ -5,7 +5,6 @@ // Created by Kevin Hermawan on 02/01/24. // -import Combine import Foundation extension OllamaKit { @@ -109,7 +108,12 @@ extension OllamaKit { } } } - +} + +#if canImport(Combine) +import Combine + +extension OllamaKit { /// Establishes a Combine publisher for streaming chat responses from the Ollama API, based on the provided data. /// /// This method sets up a streaming connection using the Combine framework, facilitating real-time data handling as chat responses are generated by the Ollama API. @@ -205,3 +209,4 @@ extension OllamaKit { } } } +#endif diff --git a/Sources/OllamaKit/OllamaKit+CopyModel.swift b/Sources/OllamaKit/OllamaKit+CopyModel.swift index af63a6d..a08c845 100644 --- a/Sources/OllamaKit/OllamaKit+CopyModel.swift +++ b/Sources/OllamaKit/OllamaKit+CopyModel.swift @@ -5,7 +5,6 @@ // Created by Kevin Hermawan on 01/01/24. // -import Combine import Foundation extension OllamaKit { @@ -26,7 +25,12 @@ extension OllamaKit { try await OKHTTPClient.shared.send(request: request) } - +} + +#if canImport(Combine) +import Combine + +extension OllamaKit { /// Requests the Ollama API to copy a model, returning the result as a Combine publisher. /// /// This method provides a reactive approach to request a model copy operation. It accepts ``OKCopyModelRequestData`` and returns a Combine publisher that completes when the copy operation is finished. @@ -56,3 +60,4 @@ extension OllamaKit { } } } +#endif diff --git a/Sources/OllamaKit/OllamaKit+DeleteModel.swift b/Sources/OllamaKit/OllamaKit+DeleteModel.swift index 82a6916..73cec4c 100644 --- a/Sources/OllamaKit/OllamaKit+DeleteModel.swift +++ b/Sources/OllamaKit/OllamaKit+DeleteModel.swift @@ -5,7 +5,6 @@ // Created by Kevin Hermawan on 01/01/24. // -import Combine import Foundation extension OllamaKit { @@ -26,7 +25,12 @@ extension OllamaKit { try await OKHTTPClient.shared.send(request: request) } - +} + +#if canImport(Combine) +import Combine + +extension OllamaKit { /// Requests the Ollama API to delete a specific model, returning the result as a Combine publisher. /// /// This method provides a reactive approach to request a model deletion operation. It accepts ``OKDeleteModelRequestData`` and returns a Combine publisher that completes when the deletion operation is finished. @@ -56,3 +60,4 @@ extension OllamaKit { } } } +#endif diff --git a/Sources/OllamaKit/OllamaKit+Embeddings.swift b/Sources/OllamaKit/OllamaKit+Embeddings.swift index 85d457f..07e648e 100644 --- a/Sources/OllamaKit/OllamaKit+Embeddings.swift +++ b/Sources/OllamaKit/OllamaKit+Embeddings.swift @@ -5,7 +5,6 @@ // Created by Paul Thrasher on 02/09/24. // -import Combine import Foundation extension OllamaKit { @@ -27,7 +26,12 @@ extension OllamaKit { return try await OKHTTPClient.shared.send(request: request, with: OKEmbeddingsResponse.self) } - +} + +#if canImport(Combine) +import Combine + +extension OllamaKit { /// Retrieves embeddings from a specific model from the Ollama API as a Combine publisher. /// /// This method provides a reactive approach to generate embeddings. It accepts ``OKEmbeddingsRequestData`` and returns a Combine publisher that emits an ``OKEmbeddingsResponse`` upon successful retrieval. @@ -57,3 +61,4 @@ extension OllamaKit { } } } +#endif diff --git a/Sources/OllamaKit/OllamaKit+Generate.swift b/Sources/OllamaKit/OllamaKit+Generate.swift index 6c8d9b8..6af7be6 100644 --- a/Sources/OllamaKit/OllamaKit+Generate.swift +++ b/Sources/OllamaKit/OllamaKit+Generate.swift @@ -5,7 +5,6 @@ // Created by Kevin Hermawan on 02/01/24. // -import Combine import Foundation extension OllamaKit { @@ -41,7 +40,12 @@ extension OllamaKit { } } } - +} + +#if canImport(Combine) +import Combine + +extension OllamaKit { /// Establishes a Combine publisher for streaming responses from the Ollama API, based on the provided data. /// /// This method sets up a streaming connection using the Combine framework, allowing for real-time data handling as the responses are generated by the Ollama API. @@ -71,3 +75,4 @@ extension OllamaKit { } } } +#endif diff --git a/Sources/OllamaKit/OllamaKit+ModelInfo.swift b/Sources/OllamaKit/OllamaKit+ModelInfo.swift index cabb515..4d63183 100644 --- a/Sources/OllamaKit/OllamaKit+ModelInfo.swift +++ b/Sources/OllamaKit/OllamaKit+ModelInfo.swift @@ -5,7 +5,6 @@ // Created by Kevin Hermawan on 01/01/24. // -import Combine import Foundation extension OllamaKit { @@ -27,7 +26,12 @@ extension OllamaKit { return try await OKHTTPClient.shared.send(request: request, with: OKModelInfoResponse.self) } - +} + +#if canImport(Combine) +import Combine + +extension OllamaKit { /// Retrieves detailed information for a specific model from the Ollama API as a Combine publisher. /// /// This method provides a reactive approach to fetch detailed model information. It accepts ``OKModelInfoRequestData`` and returns a Combine publisher that emits an ``OKModelInfoResponse`` upon successful retrieval. @@ -57,3 +61,4 @@ extension OllamaKit { } } } +#endif diff --git a/Sources/OllamaKit/OllamaKit+Models.swift b/Sources/OllamaKit/OllamaKit+Models.swift index 6447301..9b48fb0 100644 --- a/Sources/OllamaKit/OllamaKit+Models.swift +++ b/Sources/OllamaKit/OllamaKit+Models.swift @@ -5,7 +5,6 @@ // Created by Kevin Hermawan on 01/01/24. // -import Combine import Foundation extension OllamaKit { @@ -25,7 +24,12 @@ extension OllamaKit { return try await OKHTTPClient.shared.send(request: request, with: OKModelResponse.self) } - +} + +#if canImport(Combine) +import Combine + +extension OllamaKit { /// Retrieves a list of available models from the Ollama API as a Combine publisher. /// /// This method provides a reactive approach to fetch model data, returning a Combine publisher that emits an ``OKModelResponse`` with details of available models. @@ -53,3 +57,4 @@ extension OllamaKit { } } } +#endif diff --git a/Sources/OllamaKit/OllamaKit+PullModel.swift b/Sources/OllamaKit/OllamaKit+PullModel.swift index 3657421..e351174 100644 --- a/Sources/OllamaKit/OllamaKit+PullModel.swift +++ b/Sources/OllamaKit/OllamaKit+PullModel.swift @@ -5,7 +5,6 @@ // Created by Lukas Pistrol on 25.11.24. // -import Combine import Foundation extension OllamaKit { @@ -40,7 +39,12 @@ extension OllamaKit { } } } +} + +#if canImport(Combine) +import Combine +extension OllamaKit { /// Establishes a Combine publisher for pulling a model from the ollama library. /// /// This method will periodically yield ``OKPullModelResponse`` structures as the model is being pulled. @@ -74,5 +78,5 @@ extension OllamaKit { return Fail(error: error).eraseToAnyPublisher() } } - } +#endif diff --git a/Sources/OllamaKit/OllamaKit+Reachable.swift b/Sources/OllamaKit/OllamaKit+Reachable.swift index 20b9b4d..b472e62 100644 --- a/Sources/OllamaKit/OllamaKit+Reachable.swift +++ b/Sources/OllamaKit/OllamaKit+Reachable.swift @@ -5,7 +5,6 @@ // Created by Kevin Hermawan on 01/01/24. // -import Combine import Foundation extension OllamaKit { @@ -29,7 +28,12 @@ extension OllamaKit { return false } } - +} + +#if canImport(Combine) +import Combine + +extension OllamaKit { /// Checks if the Ollama API is reachable, returning the result as a Combine publisher. /// /// This method performs a network request to the Ollama API and returns a Combine publisher that emits `true` if the API is reachable. In case of any errors, it emits `false`. @@ -58,3 +62,4 @@ extension OllamaKit { } } } +#endif diff --git a/Sources/OllamaKit/Utils/OKHTTPClient.swift b/Sources/OllamaKit/Utils/OKHTTPClient.swift index 85c702d..78c9db8 100644 --- a/Sources/OllamaKit/Utils/OKHTTPClient.swift +++ b/Sources/OllamaKit/Utils/OKHTTPClient.swift @@ -5,7 +5,6 @@ // Created by Kevin Hermawan on 08/06/24. // -import Combine import Foundation internal struct OKHTTPClient { @@ -66,6 +65,9 @@ internal extension OKHTTPClient { } } +#if canImport(Combine) +import Combine + internal extension OKHTTPClient { func send(request: URLRequest, with responseType: T.Type) -> AnyPublisher { return URLSession.shared.dataTaskPublisher(for: request) @@ -119,6 +121,7 @@ internal extension OKHTTPClient { .eraseToAnyPublisher() } } +#endif private extension OKHTTPClient { func validate(response: URLResponse) throws { diff --git a/Sources/OllamaKit/Utils/StreamingDelegate.swift b/Sources/OllamaKit/Utils/StreamingDelegate.swift index a0ea39d..7f39aa6 100644 --- a/Sources/OllamaKit/Utils/StreamingDelegate.swift +++ b/Sources/OllamaKit/Utils/StreamingDelegate.swift @@ -5,6 +5,7 @@ // Created by Kevin Hermawan on 09/06/24. // +#if canImport(Combine) @preconcurrency import Combine import Foundation @@ -27,3 +28,4 @@ internal class StreamingDelegate: NSObject, URLSessionDataDelegate, @unchecked S subject.eraseToAnyPublisher() } } +#endif From 44563f015257e62e5936aa64204c3f09eb9f679d Mon Sep 17 00:00:00 2001 From: Philipp Date: Wed, 5 Mar 2025 19:31:50 +0100 Subject: [PATCH 2/6] Add action to test build on Ubuntu Linux --- .github/workflows/code-quality.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml index c491ca0..06913bf 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -29,3 +29,14 @@ jobs: - name: Build and test run: xcodebuild test -scheme OllamaKit -destination 'platform=macOS,arch=x86_64' + + test-linux: + runs-on: ubuntu-latest + steps: + - name: Swift version + run: swift --version + + - uses: actions/checkout@v4 + + - name: Build and test + run: swift build \ No newline at end of file From 92124e31f2f95593b53773a250c0eb077db79a59 Mon Sep 17 00:00:00 2001 From: Philipp Date: Wed, 5 Mar 2025 19:40:25 +0100 Subject: [PATCH 3/6] Use "FoundationNetworking" to fix missing `URLRequest` --- Sources/OllamaKit/Utils/OKHTTPClient.swift | 3 +++ Sources/OllamaKit/Utils/OKRouter.swift | 3 +++ 2 files changed, 6 insertions(+) diff --git a/Sources/OllamaKit/Utils/OKHTTPClient.swift b/Sources/OllamaKit/Utils/OKHTTPClient.swift index 78c9db8..6385efe 100644 --- a/Sources/OllamaKit/Utils/OKHTTPClient.swift +++ b/Sources/OllamaKit/Utils/OKHTTPClient.swift @@ -6,6 +6,9 @@ // import Foundation +#if canImport(FoundationNetworking) +import FoundationNetworking +#endif internal struct OKHTTPClient { private let decoder: JSONDecoder = .default diff --git a/Sources/OllamaKit/Utils/OKRouter.swift b/Sources/OllamaKit/Utils/OKRouter.swift index 0e3d280..e5eb885 100644 --- a/Sources/OllamaKit/Utils/OKRouter.swift +++ b/Sources/OllamaKit/Utils/OKRouter.swift @@ -6,6 +6,9 @@ // import Foundation +#if canImport(FoundationNetworking) +import FoundationNetworking +#endif internal enum OKRouter { case root From ca5d0ce1fa01cb47fcc48911a9abd9305319de92 Mon Sep 17 00:00:00 2001 From: Philipp Date: Thu, 6 Mar 2025 22:10:00 +0100 Subject: [PATCH 4/6] Alternative implementation of `stream` which might work on Linux. --- Sources/OllamaKit/Utils/OKHTTPClient.swift | 89 +++++++++++++++++++++- 1 file changed, 88 insertions(+), 1 deletion(-) diff --git a/Sources/OllamaKit/Utils/OKHTTPClient.swift b/Sources/OllamaKit/Utils/OKHTTPClient.swift index 6385efe..3223531 100644 --- a/Sources/OllamaKit/Utils/OKHTTPClient.swift +++ b/Sources/OllamaKit/Utils/OKHTTPClient.swift @@ -15,6 +15,50 @@ internal struct OKHTTPClient { static let shared = OKHTTPClient() } +#if canImport(FoundationNetworking) +fileprivate final class DataTaskDelegate: NSObject, URLSessionDataDelegate { + + let urlResponseCallback: (@Sendable (URLResponse) -> Void)? + let dataCallback: (@Sendable (Data) -> Void)? + let completionCallback: (@Sendable (Error?) -> Void)? + + init(urlResponseCallback: (@Sendable (URLResponse) -> Void)?, + dataCallback: (@Sendable (Data) -> Void)?, + completionCallback: (@Sendable (Error?) -> Void)? + ) { + self.urlResponseCallback = urlResponseCallback + self.dataCallback = dataCallback + self.completionCallback = completionCallback + } + + func urlSession( + _ session: URLSession, + dataTask: URLSessionDataTask, + didReceive response: URLResponse, + completionHandler: @escaping (URLSession.ResponseDisposition) -> Void + ) { + urlResponseCallback?(response) + completionHandler(.allow) + } + + + func urlSession(_ session: URLSession, + dataTask: URLSessionDataTask, + didReceive data: Data) { + // Handle the incoming data chunk here + dataCallback?(data) + } + + + func urlSession(_ session: URLSession, + task: URLSessionTask, + didCompleteWithError error: Error?) { + // Handle completion or errors + completionCallback?(error) + } +} +#endif + internal extension OKHTTPClient { func send(request: URLRequest) async throws -> Void { let (_, response) = try await URLSession.shared.data(for: request) @@ -28,7 +72,49 @@ internal extension OKHTTPClient { return try decoder.decode(T.self, from: data) } - + + #if canImport(FoundationNetworking) + func stream(request: URLRequest, with responseType: T.Type) -> AsyncThrowingStream { + return AsyncThrowingStream { continuation in + Task { + let task = URLSession.shared.dataTask(with: request) + let delegate = DataTaskDelegate( + urlResponseCallback: { response in + do { + try validate(response: response) + } catch { + continuation.finish(throwing: error) + } + }, + dataCallback: { data in + do { + let decodedObject = try self.decoder.decode(T.self, from: data) + continuation.yield(decodedObject) + } catch { + continuation.finish(throwing: error) + } + }, + completionCallback: { error in + if let error { + continuation.finish(throwing: error) + } + continuation.finish() + } + ) + task.delegate = delegate + + continuation.onTermination = { terminationState in + // Cancellation of our task should cancel the URLSessionDataTask + if case .cancelled = terminationState { + task.cancel() + } + } + + task.resume() + } + } + } + #else func stream(request: URLRequest, with responseType: T.Type) -> AsyncThrowingStream { return AsyncThrowingStream { continuation in Task { @@ -66,6 +152,7 @@ internal extension OKHTTPClient { } } } + #endif } #if canImport(Combine) From 7571adc2044b4122feadd1097188a5c1e5a8e0cd Mon Sep 17 00:00:00 2001 From: Philipp Date: Sun, 9 Mar 2025 11:15:04 +0100 Subject: [PATCH 5/6] Remove `DataTaskDelegate` and reuse `StreamingDelegate` (but adapted "no Combine") --- Sources/OllamaKit/Utils/OKHTTPClient.swift | 62 ++++--------------- .../OllamaKit/Utils/StreamingDelegate.swift | 50 ++++++++++++++- 2 files changed, 60 insertions(+), 52 deletions(-) diff --git a/Sources/OllamaKit/Utils/OKHTTPClient.swift b/Sources/OllamaKit/Utils/OKHTTPClient.swift index 3223531..a99d1ba 100644 --- a/Sources/OllamaKit/Utils/OKHTTPClient.swift +++ b/Sources/OllamaKit/Utils/OKHTTPClient.swift @@ -15,50 +15,6 @@ internal struct OKHTTPClient { static let shared = OKHTTPClient() } -#if canImport(FoundationNetworking) -fileprivate final class DataTaskDelegate: NSObject, URLSessionDataDelegate { - - let urlResponseCallback: (@Sendable (URLResponse) -> Void)? - let dataCallback: (@Sendable (Data) -> Void)? - let completionCallback: (@Sendable (Error?) -> Void)? - - init(urlResponseCallback: (@Sendable (URLResponse) -> Void)?, - dataCallback: (@Sendable (Data) -> Void)?, - completionCallback: (@Sendable (Error?) -> Void)? - ) { - self.urlResponseCallback = urlResponseCallback - self.dataCallback = dataCallback - self.completionCallback = completionCallback - } - - func urlSession( - _ session: URLSession, - dataTask: URLSessionDataTask, - didReceive response: URLResponse, - completionHandler: @escaping (URLSession.ResponseDisposition) -> Void - ) { - urlResponseCallback?(response) - completionHandler(.allow) - } - - - func urlSession(_ session: URLSession, - dataTask: URLSessionDataTask, - didReceive data: Data) { - // Handle the incoming data chunk here - dataCallback?(data) - } - - - func urlSession(_ session: URLSession, - task: URLSessionTask, - didCompleteWithError error: Error?) { - // Handle completion or errors - completionCallback?(error) - } -} -#endif - internal extension OKHTTPClient { func send(request: URLRequest) async throws -> Void { let (_, response) = try await URLSession.shared.data(for: request) @@ -78,7 +34,8 @@ internal extension OKHTTPClient { return AsyncThrowingStream { continuation in Task { let task = URLSession.shared.dataTask(with: request) - let delegate = DataTaskDelegate( + + let delegate = StreamingDelegate( urlResponseCallback: { response in do { try validate(response: response) @@ -86,12 +43,15 @@ internal extension OKHTTPClient { continuation.finish(throwing: error) } }, - dataCallback: { data in - do { - let decodedObject = try self.decoder.decode(T.self, from: data) - continuation.yield(decodedObject) - } catch { - continuation.finish(throwing: error) + dataCallback: { buffer in // + while let chunk = self.extractNextJSON(from: &buffer) { + do { + let decodedObject = try self.decoder.decode(T.self, from: chunk) + continuation.yield(decodedObject) + } catch { + continuation.finish(throwing: error) + return + } } }, completionCallback: { error in diff --git a/Sources/OllamaKit/Utils/StreamingDelegate.swift b/Sources/OllamaKit/Utils/StreamingDelegate.swift index 7f39aa6..9153923 100644 --- a/Sources/OllamaKit/Utils/StreamingDelegate.swift +++ b/Sources/OllamaKit/Utils/StreamingDelegate.swift @@ -5,9 +5,9 @@ // Created by Kevin Hermawan on 09/06/24. // +import Foundation #if canImport(Combine) @preconcurrency import Combine -import Foundation internal class StreamingDelegate: NSObject, URLSessionDataDelegate, @unchecked Sendable { private let subject = PassthroughSubject() @@ -29,3 +29,51 @@ internal class StreamingDelegate: NSObject, URLSessionDataDelegate, @unchecked S } } #endif + +#if canImport(FoundationNetworking) +import FoundationNetworking +internal final class StreamingDelegate: NSObject, URLSessionDataDelegate, @unchecked Sendable { + + let urlResponseCallback: (@Sendable (URLResponse) -> Void)? + let dataCallback: (@Sendable (inout Data) -> Void)? + let completionCallback: (@Sendable (Error?) -> Void)? + + var buffer = Data() + + init(urlResponseCallback: (@Sendable (URLResponse) -> Void)?, + dataCallback: (@Sendable (inout Data) -> Void)?, + completionCallback: (@Sendable (Error?) -> Void)? + ) { + self.urlResponseCallback = urlResponseCallback + self.dataCallback = dataCallback + self.completionCallback = completionCallback + } + + func urlSession( + _ session: URLSession, + dataTask: URLSessionDataTask, + didReceive response: URLResponse, + completionHandler: @escaping (URLSession.ResponseDisposition) -> Void + ) { + // Handle the URLResponse here + urlResponseCallback?(response) + completionHandler(.allow) + } + + func urlSession(_ session: URLSession, + dataTask: URLSessionDataTask, + didReceive data: Data) { + buffer.append(data) + + // Handle the incoming data chunk here + dataCallback?(&buffer) + } + + func urlSession(_ session: URLSession, + task: URLSessionTask, + didCompleteWithError error: Error?) { + // Handle completion or errors + completionCallback?(error) + } +} +#endif From 05c7d8c7a2155be94cdadc412de8e19aeefb0bbf Mon Sep 17 00:00:00 2001 From: Philipp Date: Fri, 14 Mar 2025 11:28:14 +0100 Subject: [PATCH 6/6] Use custom `URLSession` for streaming otherwise delegate is never called --- Sources/OllamaKit/Utils/OKHTTPClient.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/OllamaKit/Utils/OKHTTPClient.swift b/Sources/OllamaKit/Utils/OKHTTPClient.swift index a99d1ba..088e224 100644 --- a/Sources/OllamaKit/Utils/OKHTTPClient.swift +++ b/Sources/OllamaKit/Utils/OKHTTPClient.swift @@ -33,8 +33,6 @@ internal extension OKHTTPClient { func stream(request: URLRequest, with responseType: T.Type) -> AsyncThrowingStream { return AsyncThrowingStream { continuation in Task { - let task = URLSession.shared.dataTask(with: request) - let delegate = StreamingDelegate( urlResponseCallback: { response in do { @@ -61,7 +59,9 @@ internal extension OKHTTPClient { continuation.finish() } ) - task.delegate = delegate + + let session = URLSession(configuration: .default, delegate: delegate, delegateQueue: .main) + let task = session.dataTask(with: request) continuation.onTermination = { terminationState in // Cancellation of our task should cancel the URLSessionDataTask