diff --git a/Demo/App/APIKeyModalView.swift b/Demo/App/APIKeyModalView.swift index 2c2620c2..06fcd559 100644 --- a/Demo/App/APIKeyModalView.swift +++ b/Demo/App/APIKeyModalView.swift @@ -13,14 +13,19 @@ struct APIKeyModalView: View { let isMandatory: Bool @Binding private var apiKey: String + @Binding private var proxy: String @State private var internalAPIKey: String + @State private var internalProxy: String public init( apiKey: Binding, + proxy: Binding, isMandatory: Bool = true ) { self._apiKey = apiKey + self._proxy = proxy self._internalAPIKey = State(initialValue: apiKey.wrappedValue) + self._internalProxy = State(initialValue: proxy.wrappedValue) self.isMandatory = isMandatory } @@ -68,12 +73,41 @@ struct APIKeyModalView: View { .background(Color.white) .clipShape(RoundedRectangle(cornerRadius: 8)) + + VStack(alignment: .leading, spacing: 8) { + Text( + "Set your proxy." + ) + .font(.caption) + } + + TextEditor( + text: $internalProxy + ) + .frame(height: 120) + .font(.caption) + .padding(8) + .background( + RoundedRectangle( + cornerRadius: 8 + ) + .stroke( + strokeColor, + lineWidth: 1 + ) + ) + .padding(4) + .background(Color.white) + .clipShape(RoundedRectangle(cornerRadius: 8)) + + if isMandatory { HStack { Spacer() Button { apiKey = internalAPIKey + proxy = internalProxy dismiss() } label: { Text( @@ -82,7 +116,7 @@ struct APIKeyModalView: View { .padding(8) } .buttonStyle(.borderedProminent) - .disabled(internalAPIKey.isEmpty) + .disabled(internalAPIKey.isEmpty && internalProxy.isEmpty || !internalAPIKey.isEmpty && !internalProxy.isEmpty) Spacer() } @@ -102,6 +136,20 @@ struct APIKeyModalView: View { } } } + .padding() + .navigationTitle("OpenAI Reverse Proxy") + .toolbar { + ToolbarItem(placement: .primaryAction) { + if isMandatory { + EmptyView() + } else { + Button("Close") { + proxy = internalProxy + dismiss() + } + } + } + } } } } @@ -109,11 +157,13 @@ struct APIKeyModalView: View { struct APIKeyModalView_Previews: PreviewProvider { struct APIKeyModalView_PreviewsContainerView: View { @State var apiKey = "" + @State var proxy = "" let isMandatory: Bool var body: some View { APIKeyModalView( apiKey: $apiKey, + proxy: $proxy, isMandatory: isMandatory ) } diff --git a/Demo/App/APIProvidedView.swift b/Demo/App/APIProvidedView.swift index 9771e1fb..8a7990d4 100644 --- a/Demo/App/APIProvidedView.swift +++ b/Demo/App/APIProvidedView.swift @@ -11,6 +11,7 @@ import SwiftUI struct APIProvidedView: View { @Binding var apiKey: String + @Binding var proxy: String @StateObject var chatStore: ChatStore @StateObject var imageStore: ImageStore @StateObject var miscStore: MiscStore @@ -21,23 +22,32 @@ struct APIProvidedView: View { init( apiKey: Binding, + proxy: Binding, idProvider: @escaping () -> String ) { self._apiKey = apiKey + self._proxy = proxy + var client: OpenAI? = nil + if apiKey.wrappedValue.isEmpty && !proxy.wrappedValue.isEmpty { + client = OpenAI(proxy: proxy.wrappedValue) + } else { + client = OpenAI(apiToken: apiKey.wrappedValue) + } + self._chatStore = StateObject( wrappedValue: ChatStore( - openAIClient: OpenAI(apiToken: apiKey.wrappedValue), + openAIClient: client!, idProvider: idProvider ) ) self._imageStore = StateObject( wrappedValue: ImageStore( - openAIClient: OpenAI(apiToken: apiKey.wrappedValue) + openAIClient: client! ) ) self._miscStore = StateObject( wrappedValue: MiscStore( - openAIClient: OpenAI(apiToken: apiKey.wrappedValue) + openAIClient: client! ) ) } @@ -54,5 +64,11 @@ struct APIProvidedView: View { imageStore.openAIClient = client miscStore.openAIClient = client } + .onChange(of: proxy) { newProxy in + let client = OpenAI(apiToken: newProxy) + chatStore.openAIClient = client + imageStore.openAIClient = client + miscStore.openAIClient = client + } } } diff --git a/Demo/App/DemoApp.swift b/Demo/App/DemoApp.swift index a11534dc..7fc6e990 100644 --- a/Demo/App/DemoApp.swift +++ b/Demo/App/DemoApp.swift @@ -12,6 +12,7 @@ import SwiftUI @main struct DemoApp: App { @AppStorage("apiKey") var apiKey: String = "" + @AppStorage("proxy") var proxy: String = "" @State var isShowingAPIConfigModal: Bool = true let idProvider: () -> String @@ -29,16 +30,17 @@ struct DemoApp: App { Group { APIProvidedView( apiKey: $apiKey, + proxy: $proxy, idProvider: idProvider ) } #if os(iOS) .fullScreenCover(isPresented: $isShowingAPIConfigModal) { - APIKeyModalView(apiKey: $apiKey) + APIKeyModalView(apiKey: $apiKey, proxy: $proxy) } #elseif os(macOS) .popover(isPresented: $isShowingAPIConfigModal) { - APIKeyModalView(apiKey: $apiKey) + APIKeyModalView(apiKey: $apiKey, proxy: $proxy) } #endif } diff --git a/Sources/OpenAI/OpenAI.swift b/Sources/OpenAI/OpenAI.swift index 49715c8e..3ae9f5eb 100644 --- a/Sources/OpenAI/OpenAI.swift +++ b/Sources/OpenAI/OpenAI.swift @@ -15,8 +15,8 @@ final public class OpenAI: OpenAIProtocol { public struct Configuration { /// OpenAI API token. See https://platform.openai.com/docs/api-reference/authentication - public let token: String - + public let token: String? + /// Optional OpenAI organization identifier. See https://platform.openai.com/docs/api-reference/authentication public let organizationIdentifier: String? @@ -26,12 +26,19 @@ final public class OpenAI: OpenAIProtocol { /// Default request timeout public let timeoutInterval: TimeInterval - public init(token: String, organizationIdentifier: String? = nil, host: String = "api.openai.com", timeoutInterval: TimeInterval = 60.0) { - self.token = token + public init(organizationIdentifier: String? = nil, host: String, timeoutInterval: TimeInterval = 60.0) { + self.token = nil self.organizationIdentifier = organizationIdentifier self.host = host self.timeoutInterval = timeoutInterval } + + public init(token: String, organizationIdentifier: String? = nil, timeoutInterval: TimeInterval = 60.0) { + self.token = token + self.organizationIdentifier = organizationIdentifier + self.host = "api.openai.com" + self.timeoutInterval = timeoutInterval + } } private let session: URLSessionProtocol @@ -39,6 +46,10 @@ final public class OpenAI: OpenAIProtocol { public let configuration: Configuration + public convenience init(proxy: String) { + self.init(configuration: Configuration(host: proxy), session: URLSession.shared) + } + public convenience init(apiToken: String) { self.init(configuration: Configuration(token: apiToken), session: URLSession.shared) } diff --git a/Sources/OpenAI/Private/JSONRequest.swift b/Sources/OpenAI/Private/JSONRequest.swift index 526f95c9..eb3c103e 100644 --- a/Sources/OpenAI/Private/JSONRequest.swift +++ b/Sources/OpenAI/Private/JSONRequest.swift @@ -25,10 +25,12 @@ final class JSONRequest { extension JSONRequest: URLRequestBuildable { - func build(token: String, organizationIdentifier: String?, timeoutInterval: TimeInterval) throws -> URLRequest { + func build(token: String?, organizationIdentifier: String?, timeoutInterval: TimeInterval) throws -> URLRequest { var request = URLRequest(url: url, timeoutInterval: timeoutInterval) request.setValue("application/json", forHTTPHeaderField: "Content-Type") - request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") + if let token { + request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") + } if let organizationIdentifier { request.setValue(organizationIdentifier, forHTTPHeaderField: "OpenAI-Organization") } diff --git a/Sources/OpenAI/Private/MultipartFormDataRequest.swift b/Sources/OpenAI/Private/MultipartFormDataRequest.swift index 13764a58..89a85194 100644 --- a/Sources/OpenAI/Private/MultipartFormDataRequest.swift +++ b/Sources/OpenAI/Private/MultipartFormDataRequest.swift @@ -25,12 +25,14 @@ final class MultipartFormDataRequest { extension MultipartFormDataRequest: URLRequestBuildable { - func build(token: String, organizationIdentifier: String?, timeoutInterval: TimeInterval) throws -> URLRequest { + func build(token: String?, organizationIdentifier: String?, timeoutInterval: TimeInterval) throws -> URLRequest { var request = URLRequest(url: url) let boundary: String = UUID().uuidString request.timeoutInterval = timeoutInterval request.httpMethod = method - request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") + if let token { + request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") + } request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type") if let organizationIdentifier { request.setValue(organizationIdentifier, forHTTPHeaderField: "OpenAI-Organization") diff --git a/Sources/OpenAI/Private/URLRequestBuildable.swift b/Sources/OpenAI/Private/URLRequestBuildable.swift index a10f3109..486b8c6e 100644 --- a/Sources/OpenAI/Private/URLRequestBuildable.swift +++ b/Sources/OpenAI/Private/URLRequestBuildable.swift @@ -14,5 +14,5 @@ protocol URLRequestBuildable { associatedtype ResultType - func build(token: String, organizationIdentifier: String?, timeoutInterval: TimeInterval) throws -> URLRequest + func build(token: String?, organizationIdentifier: String?, timeoutInterval: TimeInterval) throws -> URLRequest }