Skip to content

AsyncHTTPClient support for linux#143

Merged
mattt merged 4 commits intohuggingface:mattt/gate-linux-urlsessionfrom
SolbachLeads:bugfix/linux-url-session-2
Mar 23, 2026
Merged

AsyncHTTPClient support for linux#143
mattt merged 4 commits intohuggingface:mattt/gate-linux-urlsessionfrom
SolbachLeads:bugfix/linux-url-session-2

Conversation

@JonasProgrammer
Copy link
Copy Markdown
Contributor

@JonasProgrammer JonasProgrammer commented Mar 5, 2026

As discussed in #127, the current workaround is not really feasible for async applications.

This PR includes an AsyncHTTPClient trait that does the following:

  • conditionally depend on AsyncHTTPClient
  • conditionally include the AsyncHTTPClient trait for EventSource
  • use typealiases to distinguish the session types

When untraited, the library should work as-is. With the trait active, the interface is changed to consume an HTTPClient as the session type.
I tried to change the business logic as little as possible, mostly implementing URLSession's extensions for HttpClient.

@KotlinFactory
Copy link
Copy Markdown
Contributor

Any updates on this perhaps?

@JonasProgrammer
Copy link
Copy Markdown
Contributor Author

Btw, as it stands now, the increased deadline fixes some of the timeouts, but not all of them. The existing HttpClient.shared instance, which is the default session with the enabled trait, has a read time out of only 90s and cannot be reconfigured.

Internally, we use modified timeouts along with another shared singleton session, but this is not ideal (AsyncHTTPClient does not expose the means of creating a proper singleton that cannot be shut down as they do with their own .shared singleton), thus not included in the PR.

If you have any suggestions regarding default timeouts etc., I'd be happy to improve the PR in that direction.

@JonasProgrammer JonasProgrammer changed the title AysyncHTTPClient support for linux AsyncHTTPClient support for linux Mar 16, 2026
@mattt mattt requested review from Copilot and mattt March 23, 2026 11:16
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an optional AsyncHTTPClient-based transport path (via SwiftPM traits) to avoid Linux URLSession/FoundationNetworking concurrency issues, while keeping the existing URLSession-based behavior when the trait is not enabled.

Changes:

  • Introduces SessionType + makeDefaultSession() to abstract over URLSession vs AsyncHTTPClient.HTTPClient.
  • Updates the network session types in model implementations to use SessionType.
  • Adds HTTPClient convenience extensions for JSON fetch + streaming/SSE, and wires conditional dependencies/traits in Package.swift.

Reviewed changes

Copilot reviewed 8 out of 9 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
Sources/AnyLanguageModel/Transport.swift Adds conditional SessionType + default session factory for URLSession vs AsyncHTTPClient.
Sources/AnyLanguageModel/Models/OpenResponsesLanguageModel.swift Switches stored session type and init default to SessionType.
Sources/AnyLanguageModel/Models/OpenAILanguageModel.swift Switches stored session type and init default to SessionType.
Sources/AnyLanguageModel/Models/OllamaLanguageModel.swift Switches stored session type and init default to SessionType.
Sources/AnyLanguageModel/Models/GeminiLanguageModel.swift Switches stored session type and init defaults to SessionType (both initializers).
Sources/AnyLanguageModel/Models/AnthropicLanguageModel.swift Switches stored session type and init default to SessionType.
Sources/AnyLanguageModel/Extensions/HTTPClient+Extensions.swift Adds AsyncHTTPClient-backed fetch, newline-stream decoding, and SSE decoding helpers.
Package.swift Adds AsyncHTTPClient trait + conditional dependencies, and forwards the trait to EventSource.
Package.resolved Locks AsyncHTTPClient + transitive NIO dependencies and bumps EventSource resolution.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +155 to +159
let asyncBytes = AsyncStream<UInt8> { byteContinuation in
SwiftTask {
do {
for try await buffer in response.body {
for byte in buffer.readableBytesView {
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In fetchEventStream, the AsyncStream<UInt8> is produced by launching an inner Task (the one iterating response.body), but that task is not stored or cancelled when the outer AsyncThrowingStream terminates. This can leave a request/body-consumption task running after the consumer cancels, keeping the connection and resources alive. Consider keeping a reference to that inner task and cancelling it (and finishing the byte stream) in an onTermination handler (or otherwise wiring cancellation through so cancelling the stream reliably stops reading the body).

Copilot uses AI. Check for mistakes.
Comment on lines +8 to +11
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
import JSONSchema
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FoundationNetworking and JSONSchema appear to be unused in this file, which will trigger unused-import warnings (and may fail CI if warnings are treated as errors). Please remove unused imports (or add uses if they’re intentional).

Suggested change
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
import JSONSchema

Copilot uses AI. Check for mistakes.
Comment on lines +197 to +205
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"
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New HTTPClientError mirrors URLSessionError, but there are no tests covering its description formatting or basic error cases under the AsyncHTTPClient trait. Since the repo already tests URLSessionError descriptions, it would be good to add equivalent tests guarded by #if canImport(AsyncHTTPClient) to prevent regressions when the trait is enabled.

Copilot uses AI. Check for mistakes.
Comment on lines +396 to 399
private let urlSession: SessionType

/// Creates an OpenAI language model.
///
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the introduction of SessionType (URLSession vs AsyncHTTPClient.HTTPClient), the private property name urlSession is now misleading when the AsyncHTTPClient trait is enabled, and it makes the initializer docs (“URL session”) inaccurate. Consider renaming the stored property to something neutral like session (and updating the parameter docs accordingly) to avoid confusion.

Copilot uses AI. Check for mistakes.
@mattt
Copy link
Copy Markdown
Collaborator

mattt commented Mar 23, 2026

Thanks so much for your work on this, @JonasProgrammer @KotlinFactory

I have some follow-up changes, but it doesn't look like I can push directly to this branch, so I'll make that post-merge.

@mattt mattt merged commit 056dccd into huggingface:mattt/gate-linux-urlsession Mar 23, 2026
4 checks passed
@@ -0,0 +1,213 @@
#if canImport(AsyncHTTPClient)
// AsyncHTTPClient.HTTPHandler introduces a Task type that clashes
typealias SwiftTask = Task
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's really too bad that Task is shadowed by AsyncHTTPClient and requires an internal typealias. Worse still that the alternative is referring to its fully-qualified _Concurrency.Task.

}
}

enum HTTPClientError: Error, CustomStringConvertible {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should probably conform to LocalizedError. I also generally like to make thrown error types public, unless this is wrapped elsewhere.

public let model: String

private let urlSession: URLSession
private let urlSession: SessionType
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bikeshed: I think this should be spelled HTTPSession

mattt added a commit that referenced this pull request Mar 23, 2026
… race (#134)

* Serialize Linux URLSession request paths to mitigate _MultiHandle race

* Incorporate feedback from review

* Replace withLock instance method with top-level withLinuxRequestLock helper

* Fix Linux compiler bug around generic returning lock helper

* More workarounds for Linux compiler bugs

* Incorporate feedback from review

* AsyncHTTPClient support for linux (#143)

* deps: add trait-based import of AsyncHTTPClient

* feat: implement transparent AsyncHTTPClient wrapper

* deps: conditionally include trait in EventSource

* Increase HTTP request timeout from 60 to 180 seconds

---------

Co-authored-by: Leonhard Solbach <49833472+KotlinFactory@users.noreply.github.com>

* Incorporate feedback from review

* swift format -i -r .

---------

Co-authored-by: Jonas Stoehr <jonass@dev.jsje.de>
Co-authored-by: Leonhard Solbach <49833472+KotlinFactory@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants