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
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 20 additions & 8 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,29 @@ on:
branches: [ "main" ]

jobs:
build_and_test:
build_and_test_macos:
runs-on: macos-latest
steps:
- uses: swift-actions/setup-swift@v2
with:
swift-version: "6.0.1"
- uses: actions/checkout@v4
- name: Get swift version
run: swift --version
- name: Build
run: swift build -q
- name: Run tests
run: swift test -q

build_and_test_linux:
runs-on: ubuntu-latest
container:
image: swift:6.0.1-jammy
steps:
- name: Install dependencies
run: |
apt-get update
apt-get install -y curl git
- uses: actions/checkout@v4
- name: Get swift version
run: swift --version
- name: Build
run: swift build -q
- name: Run tests
Expand All @@ -33,7 +47,5 @@ jobs:
uses: Homebrew/actions/setup-homebrew@master
- name: Install swiftformat
run: brew install swiftformat
- name: Run linter
run: swiftformat --config rules.swiftformat .
- name: Verify that `swiftformat --config rules.swiftformat .` did not change outputs (if it did, please re-run it and re-commit!)
run: git diff --exit-code
- name: Check formatting
run: swiftformat --config rules.swiftformat --lint .
131 changes: 131 additions & 0 deletions Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 7 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,17 @@ let package = Package(
name: "SwiftOpenAI",
targets: ["SwiftOpenAI"]),
],
dependencies: [
.package(url: "https://github.com/swift-server/async-http-client.git", from: "1.25.2"),
],
Comment on lines +19 to +21
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.

targets: [
// Targets are the basic building blocks of a package, defining a module or a test suite.
// Targets can depend on other targets in this package and products from dependencies.
.target(
name: "SwiftOpenAI"),
name: "SwiftOpenAI",
dependencies: [
.product(name: "AsyncHTTPClient", package: "async-http-client", condition: .when(platforms: [.linux])),
]),
.testTarget(
name: "SwiftOpenAITests",
dependencies: ["SwiftOpenAI"]),
Expand Down
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
<img width="1090" alt="repoOpenAI" src="https://github.com/jamesrochabrun/SwiftOpenAI/assets/5378604/51bc5736-a32f-4a9f-922e-209d950e28f7">

![iOS 15+](https://img.shields.io/badge/iOS-15%2B-blue.svg)
![macOS 13+](https://img.shields.io/badge/macOS-13%2B-blue.svg)
![watchOS 9+](https://img.shields.io/badge/watchOS-9%2B-blue.svg)
![Linux](https://img.shields.io/badge/Linux-blue.svg)
[![MIT license](https://img.shields.io/badge/License-MIT-blue.svg)](https://lbesson.mit-license.org/)
[![swift-version](https://img.shields.io/badge/swift-5.9-brightgreen.svg)](https://github.com/apple/swift)
[![swiftui-version](https://img.shields.io/badge/swiftui-brightgreen)](https://developer.apple.com/documentation/swiftui)
Expand Down Expand Up @@ -99,6 +102,14 @@ to stay on the bleeding edge.

## Compatibility

### Platform Support

SwiftOpenAI supports both Apple platforms and Linux.
- **Apple platforms** include iOS 15+, macOS 13+, and watchOS 9+.
- **Linux**: SwiftOpenAI on Linux uses AsyncHTTPClient to work around URLSession bugs in Apple's Foundation framework, and can be used with the [Vapor](https://vapor.codes/) server framework.

### OpenAI-Compatible Providers

SwiftOpenAI supports various providers that are OpenAI-compatible, including but not limited to:

- [Azure OpenAI](#azure-openai)
Expand Down
3 changes: 2 additions & 1 deletion Sources/OpenAI/AIProxy/AIProxyCertificatePinning.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
//
// Created by Lou Zell on 6/23/24.
//

#if !os(Linux)
import Foundation
import OSLog

Expand Down Expand Up @@ -181,3 +181,4 @@ private func getServerCert(secTrust: SecTrust) -> SecCertificate? {
return SecTrustGetCertificateAtIndex(secTrust, 0)
}
}
#endif
14 changes: 8 additions & 6 deletions Sources/OpenAI/AIProxy/AIProxyService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
//
// Created by Lou Zell on 3/27/24.
//

#if !os(Linux)
import Foundation

private let aiproxySecureDelegate = AIProxyCertificatePinningDelegate()
Expand Down Expand Up @@ -34,19 +34,20 @@ struct AIProxyService: OpenAIService {
organizationID: String? = nil,
debugEnabled: Bool)
{
session = URLSession(
configuration: .default,
delegate: aiproxySecureDelegate,
delegateQueue: nil)
decoder = JSONDecoder()
self.partialKey = partialKey
self.clientID = clientID
self.organizationID = organizationID
self.debugEnabled = debugEnabled
openAIEnvironment = .init(baseURL: serviceURL ?? "https://api.aiproxy.pro", proxyPath: nil, version: "v1")
httpClient = URLSessionHTTPClientAdapter(
urlSession: URLSession(
configuration: .default,
delegate: aiproxySecureDelegate,
delegateQueue: nil))
}

let session: URLSession
let httpClient: HTTPClient
let decoder: JSONDecoder
let openAIEnvironment: OpenAIEnvironment

Expand Down Expand Up @@ -1314,3 +1315,4 @@ struct AIProxyService: OpenAIService {
private let organizationID: String?

}
#endif
3 changes: 2 additions & 1 deletion Sources/OpenAI/AIProxy/Endpoint+AIProxy.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
//
// Created by Lou Zell on 3/26/24.
//

#if !os(Linux)
import DeviceCheck
import Foundation
import OSLog
Expand Down Expand Up @@ -242,3 +242,4 @@ private func copy_mac_address() -> CFData? {
return nil
}
#endif
#endif
11 changes: 8 additions & 3 deletions Sources/OpenAI/Azure/DefaultOpenAIAzureService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,21 @@
//

import Foundation
#if os(Linux)
import FoundationNetworking
#endif

// MARK: - DefaultOpenAIAzureService

final public class DefaultOpenAIAzureService: OpenAIService {

public init(
azureConfiguration: AzureOpenAIConfiguration,
urlSessionConfiguration: URLSessionConfiguration = .default,
httpClient: HTTPClient,
decoder: JSONDecoder = .init(),
debugEnabled: Bool)
{
session = URLSession(configuration: urlSessionConfiguration)
self.httpClient = httpClient
self.decoder = decoder
openAIEnvironment = OpenAIEnvironment(
baseURL: "https://\(azureConfiguration.resourceName)/openai.azure.com",
Expand All @@ -27,7 +32,7 @@ final public class DefaultOpenAIAzureService: OpenAIService {
self.debugEnabled = debugEnabled
}

public let session: URLSession
public let httpClient: HTTPClient
public let decoder: JSONDecoder
public let openAIEnvironment: OpenAIEnvironment

Expand Down
6 changes: 3 additions & 3 deletions Sources/OpenAI/LocalModelService/LocalModelService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@ struct LocalModelService: OpenAIService {
baseURL: String,
proxyPath: String? = nil,
overrideVersion: String? = nil,
configuration: URLSessionConfiguration = .default,
httpClient: HTTPClient,
decoder: JSONDecoder = .init(),
debugEnabled: Bool)
{
session = URLSession(configuration: configuration)
self.httpClient = httpClient
self.decoder = decoder
self.apiKey = apiKey
openAIEnvironment = OpenAIEnvironment(baseURL: baseURL, proxyPath: proxyPath, version: overrideVersion ?? "v1")
Expand Down Expand Up @@ -49,7 +49,7 @@ struct LocalModelService: OpenAIService {
"Currently, this API is not supported. We welcome and encourage contributions to our open-source project. Please consider opening an issue or submitting a pull request to add support for this feature.")
}

let session: URLSession
let httpClient: HTTPClient
let decoder: JSONDecoder
let openAIEnvironment: OpenAIEnvironment

Expand Down
Loading