Skip to content

Implementing cross-platform support by supporting Linux-friendly HTTP clients #143

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 9 commits into
base: main
Choose a base branch
from

Conversation

mergesort
Copy link

This PR adds support for Linux, as requested in #89. It does so by:

  • Creating a new networking layer using Apple's AsyncHTTPClient library. This is considered to be the gold-standard for Swift on Linux, and is also a solid networking layer for iOS/macOS apps, and fixes the many bugs that URLSession has on Linux platforms.
  • To ensure compatibility on Apple platforms, I've added a URLSessionHTTPClientAdapter which makes network requests through URLSession.
  • When creating a new service, there is a configurable parameter for users to specify their preferred HTTPClient. This allows maximum flexibility for end-users, in case they have a preference for networking stack.

Note

These changes are not applied to any of the code related to AIProxy. I've left everything alone there as best as I can, and simply do not compile any AIProxy code on Linux, since AIProxy does not make much sense to use in a server environment where API requests are already protected.

Please let me know what you think, happy to make any changes you request. Would love to get this merged in so I can start using SwiftOpenAI in my server-side app!

Copy link
Contributor

@gsabran gsabran left a comment

Choose a reason for hiding this comment

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

I left a few comments, mostly with the goal to not change anything on non Linux platforms other than adding a layer of indirection that is fine.

If this merges, you might want to follow up with adding some CI on Linux (maybe just build and run tests) to prevent future breakages as I don't think a lot of people would think about testing on this platform.

Comment on lines +19 to +21
dependencies: [
.package(url: "https://github.com/swift-server/async-http-client.git", from: "1.25.2"),
],
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not sure this is possible, but can we scope this to only linux for now? ie wrap in #if os(Linux) ?

Copy link
Author

Choose a reason for hiding this comment

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

I've made this change by using condition: .when(platforms: [.linux]), since #if os(Linux) doesn't work in Package.swift.

Comment on lines 50 to 51
let httpClient: HTTPClient
let session: URLSession
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not super familiar with the code, but it seems weird to see both HTTPClient and URLSession properties here.

Copy link
Contributor

Choose a reason for hiding this comment

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

why not remove the session property, the NoOpClient type, and do

self.httpClient = URLSessionHTTPClientAdapter(urlSession: URLSession(
      configuration: .default,
      delegate: aiproxySecureDelegate,
      delegateQueue: nil))

Copy link
Author

Choose a reason for hiding this comment

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

This was a good idea, removed the NoOpClient in favor of your solution!

import NIOHTTP1

/// Adapter that implements HTTPClient protocol using AsyncHTTPClient
public class AsyncHTTPClientAdapter: HTTPClient {
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe to reduce the scope of the change in this PR, what do you think of:

  • scope this code to only Linux.
  • replaces usages of AsyncHTTPClientAdapter.createDefault() by HTTPClient.createDefault()
  • add
extension HTTPClient {
  static func createDefault() -> HttpClient {
    #if os(Linux)
    AsyncHTTPClientAdapter.createDefault()
    #else
    URLSessionHTTPClientAdapter()
    #endif
  }

/// - decoder: The JSON decoder to be used for parsing API responses (default is `JSONDecoder.init()`).
/// - httpClient: The HTTPClient to be used for network calls. Defaults to `AsyncHTTPClientAdapter.createDefault()`
Copy link
Contributor

Choose a reason for hiding this comment

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

@jamesrochabrun how do you feel about changing existing APIs here?

Would you prefer to keep the existing ones available outside of Linux, and have them map to the new ones, at the cost of having two functions that mostly do the same thing, or are you fine with the breaking change?

Copy link
Owner

Choose a reason for hiding this comment

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

I think its better to have it as another initializer, I would like to avoid breaking changes if possible.


// Convert URLRequest to HTTPRequest
let httpRequest = HTTPRequest(
url: request.url!,
Copy link
Contributor

Choose a reason for hiding this comment

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

the ! always looke scary

guard let httpResponse = response as? HTTPURLResponse else {
throw APIError.requestFailed(description: "invalid response unable to get a valid HTTPURLResponse")
}
printHTTPURLResponse(httpResponse)
Copy link
Contributor

Choose a reason for hiding this comment

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

note: this is removed

Comment on lines 1185 to 1190
let httpRequest = HTTPRequest(
url: request.url!,
method: HTTPMethod(rawValue: request.httpMethod ?? "GET") ?? .get,
headers: request.allHTTPHeaderFields ?? [:],
body: request.httpBody
)
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: this is repeated several times. Maybe add an initializer in HTTPRequest from URLRequest ?

@mergesort
Copy link
Author

These are all great suggestions, I'll address them in the next couple of days.

I should have noted this but I had left in the AsyncHTTPClient code on iOS/macOS so changes could be tested by people on Apple platforms, but happy to contain everything AsyncHTTPClient-related to Linux only.

@jamesrochabrun
Copy link
Owner

Thanks @mergesort for your willingness to address @gsabran feedback!

@mergesort
Copy link
Author

Sorry for the time it took, but I believe I've addressed all of the concerns. Please let me know if I've missed anything — excited to have this functionality on the server!

@jamesrochabrun jamesrochabrun requested a review from gsabran June 7, 2025 05:24
@jamesrochabrun
Copy link
Owner

@mergesort this is awesome, thanks for addressing the feedback, I will defer to @gsabran to do the final review, but can I please ask you to add a section in the Read me in how to use this new functionality? it will be nice to show it for others. Thanks!

@AlekseyPleshkov
Copy link
Contributor

AlekseyPleshkov commented Jun 7, 2025

Sorry for the time it took, but I believe I've addressed all of the concerns. Please let me know if I've missed anything — excited to have this functionality on the server!

Thank you for your work! I have tried to build docker-image (vapor+swiftopenai) and faced with a lot of errors.

I found a couple of places in code where use UIKit/AppKit to work with images and build fails here:

  • ImageEditParameters.swift:39:8: error: cannot find 'imageData' in scope
  • CreateImageEditParameters.swift:104:17: error: cannot find 'maskData' in scope
  • other places where UIKit/AppKit importability is checked

Missing arguments error for HTTPRequest:

  • OpenAIService.swift:1015:40: error: missing arguments for parameters 'url', 'method', 'headers' in call
  • other places where HTTPRequest is created

I just wanted to highlight these places because they might be important.

@mergesort
Copy link
Author

@AlekseyPleshkov I'm so sorry about that, I had been testing against my app's server, which I plan to deploy this on. I thought everything had worked on Linux, but only now did I realize that in my haste to push a new feature last week I had commented out an internal import of the package containing SwiftOpenAI, so I was seeing successful builds when they would have failed.

I've created a docker image locally for testing, and am once again trying to resolve the issues you're seeing. You may see a few pushes from me, especially as I work through adding Linux testing on CI (CI is not a strength of mine), so please excuse me if that doesn't work yet and you get a few notifications about me pushing changes.

@jamesrochabrun Yes, will absolutely add some notes to the readme before considering this done. 🙂

@mergesort
Copy link
Author

I've fixed all the errors (tested both in Docker and on my server), updated the readme, and the code should be working as expected now. Please let me know if this all works for you — I think I've addressed all of the concerns but I'm happy to make any other changes as needed.


As I mentioned, I'm not great at CI, so I used Claude Code to update the ci.yml based on these prompts. (Where Pasted text 1 | 2 | 3 are the logs from previous CI failures.

All of the code seems to be good now, but I'm still having some issues with getting Github Actions
to pass. Can you take a look at our ci.yml Github Action (in `.github/workflows/ci.yml) and these
logs, to see if we can fix this? I suspect we need to run build-and-test on macOS and Linux, but
the lint phase should only run on macOS, but perhaps there's more to it. [Pasted text #1 +1637 
lines] [Pasted text #2 +63 lines] [Pasted text #3 +52 lines] 

Note

There was one more correction I needed to make after that, running the linter to address all of the code style requirements. I noticed that this is a recent addition to the repo, and I thought Claude made a good suggestion, to automatically run linting as part of the PR process. While it's not something to do for this PR, I figured I'd attach the suggested auto-formatting version of the ci.yml, in case you'd like to consider that for another PR.
ci-auto-format.yml.zip

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