diff --git a/.swift-version b/.swift-version new file mode 100644 index 0000000..d716112 --- /dev/null +++ b/.swift-version @@ -0,0 +1 @@ +6.2.3 \ No newline at end of file diff --git a/Package.resolved b/Package.resolved index 837d776..dfa34b0 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,13 +1,22 @@ { - "originHash" : "f7b86b800200fa069a2b288e06bafe53bc937a1851b6effeebba326a62be227e", + "originHash" : "900274c8323cd1d00e7984ae75ccb98242b1ece5a05796aecee92c87241131f5", "pins" : [ + { + "identity" : "async-http-client", + "kind" : "remoteSourceControl", + "location" : "https://github.com/swift-server/async-http-client.git", + "state" : { + "revision" : "52ed9d172018e31f2dbb46f0d4f58d66e13c281e", + "version" : "1.31.0" + } + }, { "identity" : "eventsource", "kind" : "remoteSourceControl", - "location" : "https://github.com/mattt/EventSource.git", + "location" : "https://github.com/SolbachLeads/EventSource", "state" : { - "revision" : "ca2a9d90cbe49e09b92f4b6ebd922c03ebea51d0", - "version" : "1.3.0" + "revision" : "75e12b53af96605c1bc1f1e8074147ac3720c1e7", + "version" : "100000.3.1" } }, { @@ -20,39 +29,57 @@ } }, { - "identity" : "llama.swift", + "identity" : "partialjsondecoder", "kind" : "remoteSourceControl", - "location" : "https://github.com/mattt/llama.swift", + "location" : "https://github.com/mattt/PartialJSONDecoder.git", "state" : { - "revision" : "4d57cff84ba85914baa39850157e7c27684db9c8", - "version" : "2.7966.0" + "revision" : "e4d389e6bcc6771bb988d1a8a17695d8bfa97172", + "version" : "1.0.0" } }, { - "identity" : "mlx-swift", + "identity" : "swift-algorithms", "kind" : "remoteSourceControl", - "location" : "https://github.com/ml-explore/mlx-swift", + "location" : "https://github.com/apple/swift-algorithms.git", "state" : { - "revision" : "072b684acaae80b6a463abab3a103732f33774bf", - "version" : "0.29.1" + "revision" : "87e50f483c54e6efd60e885f7f5aa946cee68023", + "version" : "1.2.1" } }, { - "identity" : "mlx-swift-lm", + "identity" : "swift-asn1", "kind" : "remoteSourceControl", - "location" : "https://github.com/ml-explore/mlx-swift-lm", + "location" : "https://github.com/apple/swift-asn1.git", "state" : { - "revision" : "5064b8c5d8ed3b0bbb71385c4124f0fc102e74a2", - "version" : "2.29.3" + "revision" : "810496cf121e525d660cd0ea89a758740476b85f", + "version" : "1.5.1" } }, { - "identity" : "partialjsondecoder", + "identity" : "swift-async-algorithms", "kind" : "remoteSourceControl", - "location" : "https://github.com/mattt/PartialJSONDecoder.git", + "location" : "https://github.com/apple/swift-async-algorithms.git", "state" : { - "revision" : "e4d389e6bcc6771bb988d1a8a17695d8bfa97172", - "version" : "1.0.0" + "revision" : "2971dd5d9f6e0515664b01044826bcea16e59fac", + "version" : "1.1.2" + } + }, + { + "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" : "24ccdeeeed4dfaae7955fcac9dbf5489ed4f1a25", + "version" : "1.18.0" } }, { @@ -65,12 +92,102 @@ } }, { - "identity" : "swift-jinja", + "identity" : "swift-configuration", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-configuration.git", + "state" : { + "revision" : "b4768bd68d8a6fb356bd372cb41905046244fcae", + "version" : "1.0.2" + } + }, + { + "identity" : "swift-crypto", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-crypto.git", + "state" : { + "revision" : "6f70fa9eab24c1fd982af18c281c4525d05e3095", + "version" : "4.2.0" + } + }, + { + "identity" : "swift-distributed-tracing", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-distributed-tracing.git", + "state" : { + "revision" : "baa932c1336f7894145cbaafcd34ce2dd0b77c97", + "version" : "1.3.1" + } + }, + { + "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" : "2778fd4e5a12a8aaa30a3ee8285f4ce54c5f3181", + "version" : "1.9.1" + } + }, + { + "identity" : "swift-nio", "kind" : "remoteSourceControl", - "location" : "https://github.com/huggingface/swift-jinja.git", + "location" : "https://github.com/apple/swift-nio.git", "state" : { - "revision" : "d81197f35f41445bc10e94600795e68c6f5e94b0", - "version" : "2.3.1" + "revision" : "9b92dcd5c22ae17016ad867852e0850f1f9f93ed", + "version" : "2.94.1" + } + }, + { + "identity" : "swift-nio-extras", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio-extras.git", + "state" : { + "revision" : "3df009d563dc9f21a5c85b33d8c2e34d2e4f8c3b", + "version" : "1.32.1" + } + }, + { + "identity" : "swift-nio-http2", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio-http2.git", + "state" : { + "revision" : "979f431f1f1e75eb61562440cb2862a70d791d3d", + "version" : "1.39.1" + } + }, + { + "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" } }, { @@ -82,6 +199,24 @@ "version" : "1.1.1" } }, + { + "identity" : "swift-service-context", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-service-context.git", + "state" : { + "revision" : "1983448fefc717a2bc2ebde5490fe99873c5b8a6", + "version" : "1.2.1" + } + }, + { + "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-syntax", "kind" : "remoteSourceControl", @@ -92,12 +227,12 @@ } }, { - "identity" : "swift-transformers", + "identity" : "swift-system", "kind" : "remoteSourceControl", - "location" : "https://github.com/huggingface/swift-transformers", + "location" : "https://github.com/apple/swift-system.git", "state" : { - "revision" : "573e5c9036c2f136b3a8a071da8e8907322403d0", - "version" : "1.1.6" + "revision" : "7c6ad0fc39d0763e0b699210e4124afd5041c5df", + "version" : "1.6.4" } } ], diff --git a/Package.swift b/Package.swift index 3916bf0..bf6e1dc 100644 --- a/Package.swift +++ b/Package.swift @@ -29,19 +29,21 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/huggingface/swift-transformers", from: "1.0.0"), - .package(url: "https://github.com/mattt/EventSource", from: "1.3.0"), + .package(url: "https://github.com/SolbachLeads/EventSource", from: "100000.3.1"), .package(url: "https://github.com/mattt/JSONSchema", from: "1.3.0"), .package(url: "https://github.com/mattt/llama.swift", .upToNextMajor(from: "2.7484.0")), .package(url: "https://github.com/mattt/PartialJSONDecoder", from: "1.0.0"), // mlx-swift-lm must be >= 2.25.5 for ToolSpec/tool calls and UserInput(chat:processing:tools:). .package(url: "https://github.com/ml-explore/mlx-swift-lm", from: "2.25.5"), .package(url: "https://github.com/swiftlang/swift-syntax", from: "600.0.0"), + .package(url: "https://github.com/swift-server/async-http-client.git", from: "1.31.0") ], targets: [ .target( name: "AnyLanguageModel", dependencies: [ .target(name: "AnyLanguageModelMacros"), + .product(name: "AsyncHTTPClient", package: "async-http-client"), .product(name: "EventSource", package: "EventSource"), .product(name: "JSONSchema", package: "JSONSchema"), .product(name: "PartialJSONDecoder", package: "PartialJSONDecoder"), diff --git a/Sources/AnyLanguageModel/Extensions/HTTPClient+Extensions.swift b/Sources/AnyLanguageModel/Extensions/HTTPClient+Extensions.swift new file mode 100644 index 0000000..9162c61 --- /dev/null +++ b/Sources/AnyLanguageModel/Extensions/HTTPClient+Extensions.swift @@ -0,0 +1,250 @@ +import AsyncHTTPClient +import EventSource +import Foundation +import JSONSchema +import NIOCore +import NIOHTTP1 + +#if canImport(FoundationNetworking) + import FoundationNetworking +#endif + +enum HTTP { + enum Method: String { + case get = "GET" + case post = "POST" + } +} + +extension HTTPClient { + func fetch( + _ method: HTTP.Method, + url: URL, + headers: [String: String] = [:], + body: Data? = nil, + dateDecodingStrategy: JSONDecoder.DateDecodingStrategy = .deferredToDate + ) async throws -> T { + var request = HTTPClientRequest(url: url.absoluteString) + request.method = method == .get ? .GET : .POST + request.headers.add(name: "Accept", value: "application/json") + + for (key, value) in headers { + request.headers.add(name: key, value: value) + } + + if let body { + request.body = .bytes(ByteBuffer(bytes: body)) + request.headers.add(name: "Content-Type", value: "application/json") + } + + let response = try await self.execute(request, timeout: .seconds(60)) + + let statusCode = Int(response.status.code) + guard (200..<300).contains(statusCode) else { + let bodyData = try await response.body.collect(upTo: 1024 * 1024) // 1MB limit + let errorString = String(buffer: bodyData) + throw HTTPClientError.httpError(statusCode: statusCode, detail: errorString) + } + + let bodyData = try await response.body.collect(upTo: 10 * 1024 * 1024) // 10MB limit + var data = Data() + data.reserveCapacity(bodyData.readableBytes) + data.append(contentsOf: bodyData.readableBytesView) + + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = dateDecodingStrategy + + do { + return try decoder.decode(T.self, from: data) + } catch { + throw HTTPClientError.decodingError(detail: error.localizedDescription) + } + } + + func fetchStream( + _ method: HTTP.Method, + url: URL, + headers: [String: String] = [:], + body: Data? = nil, + dateDecodingStrategy: JSONDecoder.DateDecodingStrategy = .deferredToDate + ) -> AsyncThrowingStream { + AsyncThrowingStream { continuation in + let task = _Concurrency.Task { @Sendable in + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = dateDecodingStrategy + + do { + var request = HTTPClientRequest(url: url.absoluteString) + request.method = method == .get ? .GET : .POST + request.headers.add(name: "Accept", value: "application/json") + + for (key, value) in headers { + request.headers.add(name: key, value: value) + } + + if let body { + request.body = .bytes(ByteBuffer(bytes: body)) + request.headers.add(name: "Content-Type", value: "application/json") + } + + let response = try await self.execute(request, timeout: .seconds(300)) + + let statusCode = Int(response.status.code) + guard (200..<300).contains(statusCode) else { + let bodyData = try await response.body.collect(upTo: 1024 * 1024) + let errorString = String(buffer: bodyData) + throw HTTPClientError.httpError(statusCode: statusCode, detail: errorString) + } + + // Collect the full body + let bodyData = try await response.body.collect(upTo: 10 * 1024 * 1024) + var buffer = Data() + buffer.reserveCapacity(bodyData.readableBytes) + buffer.append(contentsOf: bodyData.readableBytesView) + + // Process line by line + while let newlineIndex = buffer.firstIndex(of: UInt8(ascii: "\n")) { + let chunk = buffer[..( + _ method: HTTP.Method, + url: URL, + headers: [String: String] = [:], + body: Data? = nil + ) -> AsyncThrowingStream { + AsyncThrowingStream { continuation in + let task = _Concurrency.Task { @Sendable in + do { + var request = HTTPClientRequest(url: url.absoluteString) + request.method = method == .get ? .GET : .POST + request.headers.add(name: "Accept", value: "text/event-stream") + + for (key, value) in headers { + request.headers.add(name: key, value: value) + } + + if let body { + request.body = .bytes(ByteBuffer(bytes: body)) + request.headers.add(name: "Content-Type", value: "application/json") + } + + let response = try await self.execute(request, timeout: .seconds(300)) + + let statusCode = Int(response.status.code) + guard (200..<300).contains(statusCode) else { + let bodyData = try await response.body.collect(upTo: 1024 * 1024) + let errorString = String(buffer: bodyData) + throw HTTPClientError.httpError(statusCode: statusCode, detail: errorString) + } + + let decoder = JSONDecoder() + + // Convert response body to async byte stream and process server-sent events + let byteStream = ByteStreamFromHTTPBody(response.body) + for try await event in byteStream.events { + guard let data = event.data.data(using: .utf8) else { continue } + if let decoded = try? decoder.decode(T.self, from: data) { + continuation.yield(decoded) + } + } + + continuation.finish() + } catch { + continuation.finish(throwing: error) + } + } + + continuation.onTermination = { _ in + task.cancel() + } + } + } +} + +// Helper to convert HTTPClientResponse.Body to an AsyncSequence of bytes +private struct ByteStreamFromHTTPBody: AsyncSequence { + typealias Element = UInt8 + + private let body: HTTPClientResponse.Body + + init(_ body: HTTPClientResponse.Body) { + self.body = body + } + + func makeAsyncIterator() -> AsyncIterator { + AsyncIterator(body: body) + } + + struct AsyncIterator: AsyncIteratorProtocol { + private var bodyIterator: HTTPClientResponse.Body.AsyncIterator + private var currentBuffer: ByteBuffer? + private var currentIndex: Int = 0 + + init(body: HTTPClientResponse.Body) { + self.bodyIterator = body.makeAsyncIterator() + } + + mutating func next() async throws -> UInt8? { + // If we have a current buffer with remaining bytes, return the next byte + if let buffer = currentBuffer, currentIndex < buffer.readableBytes { + let byte = buffer.getInteger(at: buffer.readerIndex + currentIndex, as: UInt8.self) + currentIndex += 1 + return byte + } + + // Otherwise, get the next buffer from the body + guard let nextBuffer = try await bodyIterator.next() else { + return nil + } + + currentBuffer = nextBuffer + currentIndex = 0 + + // Return the first byte from the new buffer + if nextBuffer.readableBytes > 0 { + let byte = nextBuffer.getInteger(at: nextBuffer.readerIndex, as: UInt8.self) + currentIndex += 1 + return byte + } + + // If the buffer is empty, try again + return try await next() + } + } +} + +enum HTTPClientError: Error, CustomStringConvertible { + case invalidResponse + case httpError(statusCode: Int, detail: String) + case decodingError(detail: String) + + var description: String { + switch self { + case .invalidResponse: + return "Invalid response" + case .httpError(let statusCode, let detail): + return "HTTP error (Status \(statusCode)): \(detail)" + case .decodingError(let detail): + return "Decoding error: \(detail)" + } + } +} \ No newline at end of file diff --git a/Sources/AnyLanguageModel/Extensions/URLSession+Extensions.swift b/Sources/AnyLanguageModel/Extensions/URLSession+Extensions.swift deleted file mode 100644 index 4c6d0cd..0000000 --- a/Sources/AnyLanguageModel/Extensions/URLSession+Extensions.swift +++ /dev/null @@ -1,315 +0,0 @@ -import EventSource -import Foundation -import JSONSchema - -#if canImport(FoundationNetworking) - import FoundationNetworking -#endif - -enum HTTP { - enum Method: String { - case get = "GET" - case post = "POST" - } -} - -extension URLSession { - func fetch( - _ method: HTTP.Method, - url: URL, - headers: [String: String] = [:], - body: Data? = nil, - dateDecodingStrategy: JSONDecoder.DateDecodingStrategy = .deferredToDate - ) async throws -> T { - var request = URLRequest(url: url) - request.httpMethod = method.rawValue - request.addValue("application/json", forHTTPHeaderField: "Accept") - - for (key, value) in headers { - request.addValue(value, forHTTPHeaderField: key) - } - - if let body { - request.httpBody = body - request.addValue("application/json", forHTTPHeaderField: "Content-Type") - } - - let (data, response) = try await data(for: request) - - guard let httpResponse = response as? HTTPURLResponse else { - throw URLSessionError.invalidResponse - } - - let decoder = JSONDecoder() - decoder.dateDecodingStrategy = dateDecodingStrategy - - guard (200 ..< 300).contains(httpResponse.statusCode) else { - if let errorString = String(data: data, encoding: .utf8) { - throw URLSessionError.httpError(statusCode: httpResponse.statusCode, detail: errorString) - } - throw URLSessionError.httpError(statusCode: httpResponse.statusCode, detail: "Invalid response") - } - - do { - return try decoder.decode(T.self, from: data) - } catch { - throw URLSessionError.decodingError(detail: error.localizedDescription) - } - } - - func fetchStream( - _ method: HTTP.Method, - url: URL, - headers: [String: String] = [:], - body: Data? = nil, - dateDecodingStrategy: JSONDecoder.DateDecodingStrategy = .deferredToDate - ) -> AsyncThrowingStream { - AsyncThrowingStream { continuation in - let task = Task { @Sendable in - let decoder = JSONDecoder() - decoder.dateDecodingStrategy = dateDecodingStrategy - - do { - var request = URLRequest(url: url) - request.httpMethod = method.rawValue - request.addValue("application/json", forHTTPHeaderField: "Accept") - - for (key, value) in headers { - request.addValue(value, forHTTPHeaderField: key) - } - - if let body { - request.httpBody = body - request.addValue("application/json", forHTTPHeaderField: "Content-Type") - } - - let (data, response) = try await self.data(for: request) - - guard let httpResponse = response as? HTTPURLResponse else { - throw URLSessionError.invalidResponse - } - - guard (200 ..< 300).contains(httpResponse.statusCode) else { - if let errorString = String(data: data, encoding: .utf8) { - throw URLSessionError.httpError(statusCode: httpResponse.statusCode, detail: errorString) - } - throw URLSessionError.httpError(statusCode: httpResponse.statusCode, detail: "Invalid response") - } - - var buffer = data - - while let newlineIndex = buffer.firstIndex(of: UInt8(ascii: "\n")) { - let chunk = buffer[..( - _ method: HTTP.Method, - url: URL, - headers: [String: String] = [:], - body: Data? = nil - ) -> AsyncThrowingStream { - AsyncThrowingStream { continuation in - let task = Task { @Sendable in - do { - var request = URLRequest(url: url) - request.httpMethod = method.rawValue - request.addValue("text/event-stream", forHTTPHeaderField: "Accept") - - for (key, value) in headers { - request.addValue(value, forHTTPHeaderField: key) - } - - if let body { - request.httpBody = body - request.addValue("application/json", forHTTPHeaderField: "Content-Type") - } - - #if canImport(FoundationNetworking) - let (asyncBytes, response) = try await self.linuxBytes(for: request) - #else - let (asyncBytes, response) = try await self.bytes(for: request) - #endif - - guard let httpResponse = response as? HTTPURLResponse else { - throw URLSessionError.invalidResponse - } - - guard (200 ..< 300).contains(httpResponse.statusCode) else { - var errorData = Data() - for try await byte in asyncBytes { - errorData.append(byte) - } - if let errorString = String(data: errorData, encoding: .utf8) { - throw URLSessionError.httpError(statusCode: httpResponse.statusCode, detail: errorString) - } - throw URLSessionError.httpError(statusCode: httpResponse.statusCode, detail: "Invalid response") - } - - let decoder = JSONDecoder() - - for try await event in asyncBytes.events { - guard let data = event.data.data(using: .utf8) else { continue } - if let decoded = try? decoder.decode(T.self, from: data) { - continuation.yield(decoded) - } - } - - continuation.finish() - } catch { - continuation.finish(throwing: error) - } - } - - continuation.onTermination = { _ in - task.cancel() - } - } - } -} - -#if canImport(FoundationNetworking) - private extension URLSession { - func linuxBytes(for request: URLRequest) async throws -> (AsyncThrowingStream, URLResponse) { - let delegate = LinuxBytesDelegate() - let delegateQueue = OperationQueue() - delegateQueue.maxConcurrentOperationCount = 1 - - let session = URLSession( - configuration: self.configuration, - delegate: delegate, - delegateQueue: delegateQueue - ) - - let byteStream = AsyncThrowingStream { continuation in - delegate.attach( - continuation, - session: session - ) - } - - let response = try await delegate.start( - request: request, - session: session - ) - - return (byteStream, response) - } - } - - private final class LinuxBytesDelegate: NSObject, URLSessionDataDelegate, @unchecked Sendable { - private var responseContinuation: CheckedContinuation? - private var byteContinuation: AsyncThrowingStream.Continuation? - private weak var task: URLSessionDataTask? - private weak var session: URLSession? - - func attach( - _ continuation: AsyncThrowingStream.Continuation, - session: URLSession - ) { - byteContinuation = continuation - self.session = session - continuation.onTermination = { [weak self] _ in - guard let self else { return } - self.task?.cancel() - self.session?.invalidateAndCancel() - } - } - - func start( - request: URLRequest, - session: URLSession - ) async throws -> URLResponse { - try await withCheckedThrowingContinuation { continuation in - responseContinuation = continuation - let task = session.dataTask(with: request) - self.task = task - task.resume() - } - } - - func urlSession( - _ session: URLSession, - dataTask: URLSessionDataTask, - didReceive response: URLResponse, - completionHandler: @escaping @Sendable (URLSession.ResponseDisposition) -> Void - ) { - if let continuation = responseContinuation { - continuation.resume(returning: response) - responseContinuation = nil - } - completionHandler(.allow) - } - - func urlSession( - _ session: URLSession, - dataTask: URLSessionDataTask, - didReceive data: Data - ) { - guard let continuation = byteContinuation else { return } - for byte in data { - continuation.yield(byte) - } - } - - func urlSession( - _ session: URLSession, - task: URLSessionTask, - didCompleteWithError error: (any Error)? - ) { - if let continuation = responseContinuation { - if let error { - continuation.resume(throwing: error) - } else if let response = task.response { - continuation.resume(returning: response) - } else { - continuation.resume(throwing: URLSessionError.invalidResponse) - } - responseContinuation = nil - } - - if let error { - byteContinuation?.finish(throwing: error) - } else { - byteContinuation?.finish() - } - byteContinuation = nil - - session.invalidateAndCancel() - } - } -#endif - -enum URLSessionError: Error, CustomStringConvertible { - case invalidResponse - case httpError(statusCode: Int, detail: String) - case decodingError(detail: String) - - var description: String { - switch self { - case .invalidResponse: - return "Invalid response" - case .httpError(let statusCode, let detail): - return "HTTP error (Status \(statusCode)): \(detail)" - case .decodingError(let detail): - return "Decoding error: \(detail)" - } - } -} diff --git a/Sources/AnyLanguageModel/Models/AnthropicLanguageModel.swift b/Sources/AnyLanguageModel/Models/AnthropicLanguageModel.swift index bd21a1f..f67a8b8 100644 --- a/Sources/AnyLanguageModel/Models/AnthropicLanguageModel.swift +++ b/Sources/AnyLanguageModel/Models/AnthropicLanguageModel.swift @@ -1,3 +1,4 @@ +import AsyncHTTPClient import EventSource import Foundation import JSONSchema @@ -278,7 +279,7 @@ public struct AnthropicLanguageModel: LanguageModel { /// The model identifier to use for generation. public let model: String - private let urlSession: URLSession + private let httpClient: HTTPClient /// Creates an Anthropic language model. /// @@ -288,14 +289,14 @@ public struct AnthropicLanguageModel: LanguageModel { /// - apiVersion: The API version to use for requests. Defaults to `2023-06-01`. /// - betas: Optional beta version(s) of the API to use. /// - model: The model identifier (for example, "claude-3-5-sonnet-20241022"). - /// - session: The URL session to use for network requests. + /// - session: The HTTP client to use for network requests. If nil, uses HTTPClient.shared. public init( baseURL: URL = defaultBaseURL, apiKey tokenProvider: @escaping @autoclosure @Sendable () -> String, apiVersion: String = defaultAPIVersion, betas: [String]? = nil, model: String, - session: URLSession = URLSession(configuration: .default) + session: HTTPClient? = nil ) { var baseURL = baseURL if !baseURL.path.hasSuffix("/") { @@ -307,7 +308,7 @@ public struct AnthropicLanguageModel: LanguageModel { self.apiVersion = apiVersion self.betas = betas self.model = model - self.urlSession = session + self.httpClient = session ?? HTTPClient.shared } public func respond( @@ -337,7 +338,7 @@ public struct AnthropicLanguageModel: LanguageModel { let body = try JSONEncoder().encode(params) - let message: AnthropicMessageResponse = try await urlSession.fetch( + let message: AnthropicMessageResponse = try await httpClient.fetch( .post, url: url, headers: headers, @@ -435,7 +436,7 @@ public struct AnthropicLanguageModel: LanguageModel { // Stream server-sent events from Anthropic API let events: AsyncThrowingStream = - urlSession + httpClient .fetchEventStream( .post, url: url, diff --git a/Sources/AnyLanguageModel/Models/GeminiLanguageModel.swift b/Sources/AnyLanguageModel/Models/GeminiLanguageModel.swift index 2da15a4..ab0cf6e 100644 --- a/Sources/AnyLanguageModel/Models/GeminiLanguageModel.swift +++ b/Sources/AnyLanguageModel/Models/GeminiLanguageModel.swift @@ -1,3 +1,4 @@ +import AsyncHTTPClient import EventSource import Foundation import JSONSchema @@ -186,7 +187,7 @@ public struct GeminiLanguageModel: LanguageModel { /// Internal storage for the deprecated serverTools property. internal var _serverTools: [CustomGenerationOptions.ServerTool] - private let urlSession: URLSession + private let httpClient: HTTPClient /// Creates a new Gemini language model. /// @@ -201,7 +202,7 @@ public struct GeminiLanguageModel: LanguageModel { apiKey tokenProvider: @escaping @autoclosure @Sendable () -> String, apiVersion: String = defaultAPIVersion, model: String, - session: URLSession = URLSession(configuration: .default) + session: HTTPClient? = nil ) { var baseURL = baseURL if !baseURL.path.hasSuffix("/") { @@ -214,7 +215,7 @@ public struct GeminiLanguageModel: LanguageModel { self.model = model self._thinking = .disabled self._serverTools = [] - self.urlSession = session + self.httpClient = session ?? HTTPClient.shared } /// Creates a new Gemini language model with thinking and server tools configuration. @@ -243,7 +244,7 @@ public struct GeminiLanguageModel: LanguageModel { model: String, thinking: CustomGenerationOptions.Thinking = .disabled, serverTools: [CustomGenerationOptions.ServerTool] = [], - session: URLSession = URLSession(configuration: .default) + session: HTTPClient? = nil ) { var baseURL = baseURL if !baseURL.path.hasSuffix("/") { @@ -256,7 +257,7 @@ public struct GeminiLanguageModel: LanguageModel { self.model = model self._thinking = thinking self._serverTools = serverTools - self.urlSession = session + self.httpClient = session ?? HTTPClient.shared } public func respond( @@ -295,7 +296,7 @@ public struct GeminiLanguageModel: LanguageModel { let body = try JSONEncoder().encode(params) - let response: GeminiGenerateContentResponse = try await urlSession.fetch( + let response: GeminiGenerateContentResponse = try await httpClient.fetch( .post, url: url, headers: headers, @@ -407,7 +408,7 @@ public struct GeminiLanguageModel: LanguageModel { let body = try JSONEncoder().encode(params) let stream: AsyncThrowingStream = - urlSession + httpClient .fetchEventStream( .post, url: url, diff --git a/Sources/AnyLanguageModel/Models/OllamaLanguageModel.swift b/Sources/AnyLanguageModel/Models/OllamaLanguageModel.swift index 6be5d02..cd30c84 100644 --- a/Sources/AnyLanguageModel/Models/OllamaLanguageModel.swift +++ b/Sources/AnyLanguageModel/Models/OllamaLanguageModel.swift @@ -1,3 +1,4 @@ +import AsyncHTTPClient import Foundation import JSONSchema import OrderedCollections @@ -46,18 +47,18 @@ public struct OllamaLanguageModel: LanguageModel { /// The model identifier to use for generation. public let model: String - private let urlSession: URLSession + private let httpClient: HTTPClient /// Creates an Ollama language model. /// /// - Parameters: /// - baseURL: The base URL for the Ollama server. Defaults to `http://localhost:11434`. /// - model: The model identifier (for example, "qwen2.5" or "llama3.3"). - /// - session: The URL session to use for network requests. + /// - session: The HTTP client to use for network requests. If nil, uses HTTPClient.shared. public init( baseURL: URL = defaultBaseURL, model: String, - session: URLSession = URLSession(configuration: .default) + session: HTTPClient? = nil ) { var baseURL = baseURL if !baseURL.path.hasSuffix("/") { @@ -66,7 +67,7 @@ public struct OllamaLanguageModel: LanguageModel { self.baseURL = baseURL self.model = model - self.urlSession = session + self.httpClient = session ?? HTTPClient.shared } public func respond( @@ -105,7 +106,7 @@ public struct OllamaLanguageModel: LanguageModel { let url = baseURL.appendingPathComponent("api/chat") let body = try JSONEncoder().encode(params) - let chatResponse: ChatResponse = try await urlSession.fetch( + let chatResponse: ChatResponse = try await httpClient.fetch( .post, url: url, body: body, @@ -199,7 +200,7 @@ public struct OllamaLanguageModel: LanguageModel { // Reuse ChatResponse as each streamed line shares the same shape do { let chunks = - urlSession.fetchStream( + httpClient.fetchStream( .post, url: url, body: body, diff --git a/Sources/AnyLanguageModel/Models/OpenAILanguageModel.swift b/Sources/AnyLanguageModel/Models/OpenAILanguageModel.swift index 8828751..82548cc 100644 --- a/Sources/AnyLanguageModel/Models/OpenAILanguageModel.swift +++ b/Sources/AnyLanguageModel/Models/OpenAILanguageModel.swift @@ -1,3 +1,4 @@ +import AsyncHTTPClient import Foundation import JSONSchema @@ -393,7 +394,7 @@ public struct OpenAILanguageModel: LanguageModel { /// The API variant to use. public let apiVariant: APIVariant - private let urlSession: URLSession + private let httpClient: HTTPClient /// Creates an OpenAI language model. /// @@ -402,13 +403,13 @@ public struct OpenAILanguageModel: LanguageModel { /// - apiKey: Your OpenAI API key or a closure that returns it. /// - model: The model identifier (for example, "gpt-4" or "gpt-3.5-turbo"). /// - apiVariant: The API variant to use. Defaults to `.chatCompletions`. - /// - session: The URL session to use for network requests. + /// - session: The HTTP client to use for network requests. If nil, uses HTTPClient.shared. public init( baseURL: URL = defaultBaseURL, apiKey tokenProvider: @escaping @autoclosure @Sendable () -> String, model: String, apiVariant: APIVariant = .chatCompletions, - session: URLSession = URLSession(configuration: .default) + session: HTTPClient? = nil ) { var baseURL = baseURL if !baseURL.path.hasSuffix("/") { @@ -419,7 +420,7 @@ public struct OpenAILanguageModel: LanguageModel { self.tokenProvider = tokenProvider self.model = model self.apiVariant = apiVariant - self.urlSession = session + self.httpClient = session ?? HTTPClient.shared } public func respond( @@ -485,7 +486,7 @@ public struct OpenAILanguageModel: LanguageModel { let url = baseURL.appendingPathComponent("chat/completions") let body = try JSONEncoder().encode(params) - let resp: ChatCompletions.Response = try await urlSession.fetch( + let resp: ChatCompletions.Response = try await httpClient.fetch( .post, url: url, headers: [ @@ -593,7 +594,7 @@ public struct OpenAILanguageModel: LanguageModel { let encoder = JSONEncoder() let body = try encoder.encode(params) - let resp: Responses.Response = try await urlSession.fetch( + let resp: Responses.Response = try await httpClient.fetch( .post, url: url, headers: [ @@ -704,7 +705,7 @@ public struct OpenAILanguageModel: LanguageModel { let body = try JSONEncoder().encode(params) let events: AsyncThrowingStream = - urlSession.fetchEventStream( + httpClient.fetchEventStream( .post, url: url, headers: [ @@ -788,7 +789,7 @@ public struct OpenAILanguageModel: LanguageModel { let body = try JSONEncoder().encode(params) let events: AsyncThrowingStream = - urlSession.fetchEventStream( + httpClient.fetchEventStream( .post, url: url, headers: [ diff --git a/Sources/AnyLanguageModel/Models/OpenResponsesLanguageModel.swift b/Sources/AnyLanguageModel/Models/OpenResponsesLanguageModel.swift index c4ba51e..50c11dc 100644 --- a/Sources/AnyLanguageModel/Models/OpenResponsesLanguageModel.swift +++ b/Sources/AnyLanguageModel/Models/OpenResponsesLanguageModel.swift @@ -1,3 +1,4 @@ +import AsyncHTTPClient import Foundation import JSONSchema @@ -365,7 +366,7 @@ public struct OpenResponsesLanguageModel: LanguageModel { /// Model identifier to use for generation. public let model: String - private let urlSession: URLSession + private let httpClient: HTTPClient /// Creates an Open Responses language model. /// @@ -373,12 +374,12 @@ public struct OpenResponsesLanguageModel: LanguageModel { /// - baseURL: Base URL for the API (e.g. `https://api.openai.com/v1/` or `https://openrouter.ai/api/v1/`). Must end with `/`. /// - apiKey: API key or closure that returns it. /// - model: Model identifier (e.g. `gpt-4o-mini` or provider-specific id). - /// - session: URL session for network requests. + /// - session: The HTTP client to use for network requests. If nil, uses HTTPClient.shared. public init( baseURL: URL, apiKey tokenProvider: @escaping @autoclosure @Sendable () -> String, model: String, - session: URLSession = URLSession(configuration: .default) + session: HTTPClient? = nil ) { var baseURL = baseURL if !baseURL.path.hasSuffix("/") { @@ -387,7 +388,7 @@ public struct OpenResponsesLanguageModel: LanguageModel { self.baseURL = baseURL self.tokenProvider = tokenProvider self.model = model - self.urlSession = session + self.httpClient = session ?? HTTPClient.shared } public func respond( @@ -433,7 +434,7 @@ public struct OpenResponsesLanguageModel: LanguageModel { do { let body = try JSONEncoder().encode(params) let events: AsyncThrowingStream = - urlSession.fetchEventStream( + httpClient.fetchEventStream( .post, url: url, headers: ["Authorization": "Bearer \(tokenProvider())"], @@ -505,7 +506,7 @@ public struct OpenResponsesLanguageModel: LanguageModel { stream: false ) let body = try JSONEncoder().encode(params) - let resp: OpenResponsesAPI.Response = try await urlSession.fetch( + let resp: OpenResponsesAPI.Response = try await httpClient.fetch( .post, url: url, headers: ["Authorization": "Bearer \(tokenProvider())"],