├── Demo ├── Demo │ ├── Assets.xcassets │ │ ├── Contents.json │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ ├── Preview Content │ │ └── Preview Assets.xcassets │ │ │ └── Contents.json │ ├── DemoApp.swift │ ├── Info.plist │ └── ContentView.swift └── Demo.xcodeproj │ ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist │ └── project.pbxproj ├── Sources └── OpenAIStreamingCompletions │ ├── OpenAI.swift │ ├── EventSource │ ├── Event.swift │ ├── EventStreamParser.swift │ └── EventSource.swift │ ├── OpenAI+TextCompletion.swift │ └── OpenAI+ChatCompletion.swift ├── Tests └── OpenAIStreamingCompletionsTests │ └── OpenAIStreamingCompletionsTests.swift ├── Package.swift ├── README.md ├── .gitignore └── LICENSE /Demo/Demo/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Demo/Demo/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Demo/Demo.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "platform" : "ios", 6 | "size" : "1024x1024" 7 | } 8 | ], 9 | "info" : { 10 | "author" : "xcode", 11 | "version" : 1 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Demo/Demo/DemoApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DemoApp.swift 3 | // Demo 4 | // 5 | // Created by nate parrott on 2/23/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @main 11 | struct DemoApp: App { 12 | var body: some Scene { 13 | WindowGroup { 14 | ContentView() 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Demo/Demo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Demo/Demo/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSAppTransportSecurity 6 | 7 | NSAllowsArbitraryLoads 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /Sources/OpenAIStreamingCompletions/OpenAI.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct OpenAIAPI { 4 | var apiKey: String 5 | var orgId: String? 6 | 7 | public init(apiKey: String, orgId: String? = nil) { 8 | self.apiKey = apiKey 9 | self.orgId = orgId 10 | } 11 | } 12 | 13 | extension OpenAIAPI { 14 | enum Errors: Error { 15 | case noChoices 16 | case invalidResponse(String) 17 | case noApiKey 18 | } 19 | } 20 | 21 | -------------------------------------------------------------------------------- /Tests/OpenAIStreamingCompletionsTests/OpenAIStreamingCompletionsTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import OpenAIStreamingCompletions 3 | 4 | final class OpenAIStreamingCompletionsTests: XCTestCase { 5 | func testExample() throws { 6 | // This is an example of a functional test case. 7 | // Use XCTAssert and related functions to verify your tests produce the correct 8 | // results. 9 | XCTAssertEqual(OpenAIStreamingCompletions().text, "Hello, World!") 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.7 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "OpenAIStreamingCompletions", 8 | platforms: [ 9 | .iOS(.v13), 10 | .macOS(.v11), 11 | ], 12 | products: [ 13 | // Products define the executables and libraries a package produces, and make them visible to other packages. 14 | .library( 15 | name: "OpenAIStreamingCompletions", 16 | targets: ["OpenAIStreamingCompletions"]), 17 | ], 18 | dependencies: [ 19 | // Dependencies declare other packages that this package depends on. 20 | // .package(url: /* package url */, from: "1.0.0"), 21 | ], 22 | targets: [ 23 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 24 | // Targets can depend on other targets in this package, and on products in packages this package depends on. 25 | .target( 26 | name: "OpenAIStreamingCompletions", 27 | dependencies: []), 28 | .testTarget( 29 | name: "OpenAIStreamingCompletionsTests", 30 | dependencies: ["OpenAIStreamingCompletions"]), 31 | ] 32 | ) 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OpenAIStreamingCompletions 2 | 3 | Streaming text generartion using the OpenAI APIs. 4 | 5 | Supports streaming results via `ObservableObject` or `AsyncStream`, and non-streaming results via `async/await`. 6 | 7 | Supports message-based models (e.g. ChatGPT) and text-based models (e.g. davinci). 8 | 9 | ## Installation via Swift Package Manager 10 | 11 | You can either: 12 | - Add this line to your `Package.swift` `dependencies` array: `.package(url: "https://github.com/nate-parrott/openai-streaming-completions-swift", from: "1.0.1")` 13 | - Use Xcode's `File -> Add Packages` and paste the URL to this repository 14 | 15 | ## Calling ChatGPT 16 | 17 | ### Provide API key and prompt 18 | 19 | ``` 20 | let messages: [OpenAIAPI.Message] = [ 21 | .init(role: .system, content: "You are a helpful assistant. Answer in one sentence if possible."), 22 | .init(role: .user, content: prompt) 23 | ] 24 | let api = OpenAIAPI(apiKey: key) 25 | ``` 26 | 27 | ### Option A: Generate text (streaming, asyncStream) 28 | 29 | ``` 30 | Task { 31 | let stream = try api.completeChatStreaming(.init(messages: promptMessages)) 32 | for await message in stream { 33 | print("\(message.content)") // each message contains a small part of the response 34 | } 35 | } 36 | 37 | ``` 38 | 39 | 40 | ### Option B: Generate text (non-streaming, async/await) 41 | 42 | ``` 43 | Task { 44 | do { 45 | self.completedText = try await api.completeChat(.init(messages: messages)) 46 | } catch { 47 | print("Error: \(error)") 48 | } 49 | } 50 | ``` 51 | 52 | ## Calling other text APIs 53 | 54 | See [OpenAIAPI+TextCompletion.swift](https://github.com/nate-parrott/openai-streaming-completions-swift/blob/main/Sources/OpenAIStreamingCompletions/OpenAI%2BTextCompletion.swift) -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## User settings 6 | xcuserdata/ 7 | 8 | *.DS_Store 9 | 10 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 11 | *.xcscmblueprint 12 | *.xccheckout 13 | 14 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 15 | build/ 16 | DerivedData/ 17 | *.moved-aside 18 | *.pbxuser 19 | !default.pbxuser 20 | *.mode1v3 21 | !default.mode1v3 22 | *.mode2v3 23 | !default.mode2v3 24 | *.perspectivev3 25 | !default.perspectivev3 26 | 27 | ## Obj-C/Swift specific 28 | *.hmap 29 | 30 | ## App packaging 31 | *.ipa 32 | *.dSYM.zip 33 | *.dSYM 34 | 35 | ## Playgrounds 36 | timeline.xctimeline 37 | playground.xcworkspace 38 | 39 | # Swift Package Manager 40 | # 41 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 42 | # Packages/ 43 | # Package.pins 44 | # Package.resolved 45 | # *.xcodeproj 46 | # 47 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 48 | # hence it is not needed unless you have added a package configuration file to your project 49 | # .swiftpm 50 | 51 | .build/ 52 | 53 | # CocoaPods 54 | # 55 | # We recommend against adding the Pods directory to your .gitignore. However 56 | # you should judge for yourself, the pros and cons are mentioned at: 57 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 58 | # 59 | # Pods/ 60 | # 61 | # Add this line if you want to avoid checking in source code from the Xcode workspace 62 | # *.xcworkspace 63 | 64 | # Carthage 65 | # 66 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 67 | # Carthage/Checkouts 68 | 69 | Carthage/Build/ 70 | 71 | # Accio dependency management 72 | Dependencies/ 73 | .accio/ 74 | 75 | # fastlane 76 | # 77 | # It is recommended to not store the screenshots in the git repo. 78 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 79 | # For more information about the recommended setup visit: 80 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 81 | 82 | fastlane/report.xml 83 | fastlane/Preview.html 84 | fastlane/screenshots/**/*.png 85 | fastlane/test_output 86 | 87 | # Code Injection 88 | # 89 | # After new code Injection tools there's a generated folder /iOSInjectionProject 90 | # https://github.com/johnno1962/injectionforxcode 91 | 92 | iOSInjectionProject/ 93 | -------------------------------------------------------------------------------- /Demo/Demo/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // Demo 4 | // 5 | // Created by nate parrott on 2/23/23. 6 | // 7 | 8 | import SwiftUI 9 | import OpenAIStreamingCompletions 10 | 11 | struct ContentView: View { 12 | @State private var prompt = "what is internet explorer" 13 | @State private var completion: StreamingCompletion? 14 | @State private var completedText: String = "" 15 | @AppStorage("key") private var key = "" 16 | 17 | var body: some View { 18 | Form { 19 | Section { 20 | TextField("API key", text: $key) 21 | TextField("Prompt to complete", text: $prompt, onCommit: complete) 22 | Button("Complete Text", action: complete) 23 | Button("Complete Chat", action: completeChat) 24 | } 25 | if let completion { 26 | Section { 27 | CompletionView(completion: completion) 28 | } 29 | } 30 | if completedText != "" { 31 | Section { 32 | Text(completedText) 33 | } 34 | } 35 | } 36 | } 37 | 38 | private func complete() { 39 | if key == "" { return } 40 | self.completion = try! OpenAIAPI(apiKey: key).completeStreaming(.init(prompt: prompt, max_tokens: 256)) 41 | } 42 | 43 | private func completeChat() { 44 | if key == "" { return } 45 | let messages: [OpenAIAPI.Message] = [ 46 | .init(role: .system, content: "You are a helpful assistant. Answer in one sentence if possible."), 47 | .init(role: .user, content: prompt) 48 | ] 49 | // Task { 50 | // do { 51 | // self.completedText = try await OpenAIAPI(apiKey: key).completeChat(.init(messages: messages)) 52 | // } catch { 53 | // self.completedText = "Error: \(error)" 54 | // } 55 | // } 56 | self.completion = try! OpenAIAPI(apiKey: key).completeChatStreamingWithObservableObject(.init(messages: messages)) 57 | } 58 | } 59 | 60 | private struct CompletionView: View { 61 | @ObservedObject var completion: StreamingCompletion 62 | 63 | var body: some View { 64 | Group { 65 | Text("\(completion.text)") 66 | .multilineTextAlignment(.leading) 67 | .lineLimit(nil) 68 | } 69 | switch completion.status { 70 | case .error: Text("Errror") 71 | case .complete: Text("Complete") 72 | case .loading: Text("Loading") 73 | } 74 | } 75 | } 76 | 77 | struct ContentView_Previews: PreviewProvider { 78 | static var previews: some View { 79 | ContentView() 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /Sources/OpenAIStreamingCompletions/EventSource/Event.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Event.swift 3 | // EventSource 4 | // 5 | // Created by Andres on 01/06/2019. 6 | // Copyright © 2019 inaka. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | enum Event { 12 | case event(id: String?, event: String?, data: String?, time: String?) 13 | 14 | init?(eventString: String?, newLineCharacters: [String]) { 15 | guard let eventString = eventString else { return nil } 16 | 17 | if eventString.hasPrefix(":") { 18 | return nil 19 | } 20 | 21 | self = Event.parseEvent(eventString, newLineCharacters: newLineCharacters) 22 | } 23 | 24 | var id: String? { 25 | guard case let .event(eventId, _, _, _) = self else { return nil } 26 | return eventId 27 | } 28 | 29 | var event: String? { 30 | guard case let .event(_, eventName, _, _) = self else { return nil } 31 | return eventName 32 | } 33 | 34 | var data: String? { 35 | guard case let .event(_, _, eventData, _) = self else { return nil } 36 | return eventData 37 | } 38 | 39 | var retryTime: Int? { 40 | guard case let .event(_, _, _, aTime) = self, let time = aTime else { return nil } 41 | return Int(time.trimmingCharacters(in: CharacterSet.whitespaces)) 42 | } 43 | 44 | var onlyRetryEvent: Bool? { 45 | guard case let .event(id, name, data, time) = self else { return nil } 46 | let otherThanTime = id ?? name ?? data 47 | 48 | if otherThanTime == nil && time != nil { 49 | return true 50 | } 51 | 52 | return false 53 | 54 | } 55 | } 56 | 57 | private extension Event { 58 | 59 | static func parseEvent(_ eventString: String, newLineCharacters: [String]) -> Event { 60 | var event: [String: String?] = [:] 61 | 62 | for line in eventString.components(separatedBy: CharacterSet.newlines) as [String] { 63 | let (akey, value) = Event.parseLine(line, newLineCharacters: newLineCharacters) 64 | guard let key = akey else { continue } 65 | 66 | if let value = value, let previousValue = event[key] ?? nil { 67 | event[key] = "\(previousValue)\n\(value)" 68 | } else if let value = value { 69 | event[key] = value 70 | } else { 71 | event[key] = nil 72 | } 73 | } 74 | 75 | // the only possible field names for events are: id, event and data. Everything else is ignored. 76 | return .event( 77 | id: event["id"] ?? nil, 78 | event: event["event"] ?? nil, 79 | data: event["data"] ?? nil, 80 | time: event["retry"] ?? nil 81 | ) 82 | } 83 | 84 | static func parseLine(_ line: String, newLineCharacters: [String]) -> (key: String?, value: String?) { 85 | var key: NSString?, value: NSString? 86 | let scanner = Scanner(string: line) 87 | scanner.scanUpTo(":", into: &key) 88 | scanner.scanString(":", into: nil) 89 | 90 | for newline in newLineCharacters { 91 | if scanner.scanUpTo(newline, into: &value) { 92 | break 93 | } 94 | } 95 | 96 | // for id and data if they come empty they should return an empty string value. 97 | if key != "event" && value == nil { 98 | value = "" 99 | } 100 | 101 | return (key as String?, value as String?) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /Sources/OpenAIStreamingCompletions/EventSource/EventStreamParser.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EventStreamParser.swift 3 | // EventSource 4 | // 5 | // Created by Andres on 30/05/2019. 6 | // Copyright © 2019 inaka. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | final class EventStreamParser { 12 | 13 | // Events are separated by end of line. End of line can be: 14 | // \r = CR (Carriage Return) → Used as a new line character in Mac OS before X 15 | // \n = LF (Line Feed) → Used as a new line character in Unix/Mac OS X 16 | // \r\n = CR + LF → Used as a new line character in Windows 17 | private let validNewlineCharacters = ["\r\n", "\n", "\r"] 18 | private let dataBuffer: NSMutableData 19 | 20 | init() { 21 | dataBuffer = NSMutableData() 22 | } 23 | 24 | var currentBuffer: String? { 25 | return NSString(data: dataBuffer as Data, encoding: String.Encoding.utf8.rawValue) as String? 26 | } 27 | 28 | func append(data: Data?) -> [Event] { 29 | guard let data = data else { return [] } 30 | dataBuffer.append(data) 31 | 32 | let events = extractEventsFromBuffer().compactMap { [weak self] eventString -> Event? in 33 | guard let self = self else { return nil } 34 | return Event(eventString: eventString, newLineCharacters: self.validNewlineCharacters) 35 | } 36 | 37 | return events 38 | } 39 | 40 | private func extractEventsFromBuffer() -> [String] { 41 | var events = [String]() 42 | 43 | var searchRange = NSRange(location: 0, length: dataBuffer.length) 44 | while let foundRange = searchFirstEventDelimiter(in: searchRange) { 45 | // if we found a delimiter range that means that from the beggining of the buffer 46 | // until the beggining of the range where the delimiter was found we have an event. 47 | // The beggining of the event is: searchRange.location 48 | // The lenght of the event is the position where the foundRange was found. 49 | 50 | let dataChunk = dataBuffer.subdata( 51 | with: NSRange(location: searchRange.location, length: foundRange.location - searchRange.location) 52 | ) 53 | 54 | if let text = String(bytes: dataChunk, encoding: .utf8) { 55 | events.append(text) 56 | } 57 | 58 | // We move the searchRange start position (location) after the fundRange we just found and 59 | searchRange.location = foundRange.location + foundRange.length 60 | searchRange.length = dataBuffer.length - searchRange.location 61 | } 62 | 63 | // We empty the piece of the buffer we just search in. 64 | dataBuffer.replaceBytes(in: NSRange(location: 0, length: searchRange.location), withBytes: nil, length: 0) 65 | 66 | return events 67 | } 68 | 69 | // This methods returns the range of the first delimiter found in the buffer. For example: 70 | // If in the buffer we have: `id: event-id-1\ndata:event-data-first\n\n` 71 | // This method will return the range for the `\n\n`. 72 | private func searchFirstEventDelimiter(in range: NSRange) -> NSRange? { 73 | let delimiters = validNewlineCharacters.map { "\($0)\($0)".data(using: String.Encoding.utf8)! } 74 | 75 | for delimiter in delimiters { 76 | let foundRange = dataBuffer.range( 77 | of: delimiter, options: NSData.SearchOptions(), in: range 78 | ) 79 | 80 | if foundRange.location != NSNotFound { 81 | return foundRange 82 | } 83 | } 84 | 85 | return nil 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /Sources/OpenAIStreamingCompletions/OpenAI+TextCompletion.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Combine 3 | 4 | extension OpenAIAPI { 5 | public struct CompletionRequest: Codable { 6 | var prompt: String 7 | var model = "text-davinci-003" 8 | var max_tokens: Int = 1500 9 | var temperature: Double = 0.2 10 | var stream = false 11 | var stop: [String]? 12 | 13 | public init(prompt: String, model: String = "text-davinci-003", max_tokens: Int = 1500, temperature: Double = 0.2, stop: [String]? = nil) { 14 | self.prompt = prompt 15 | self.model = model 16 | self.max_tokens = max_tokens 17 | self.temperature = temperature 18 | self.stop = stop 19 | } 20 | } 21 | 22 | struct CompletionResponse: Codable { 23 | struct Choice: Codable { 24 | var text: String 25 | } 26 | var choices: [Choice] 27 | } 28 | 29 | public func complete(_ completionRequest: CompletionRequest) async throws -> String { 30 | let request = try createTextRequest(completionRequest: completionRequest) 31 | let (data, response) = try await URLSession.shared.data(for: request) 32 | guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else { 33 | throw Errors.invalidResponse(String(data: data, encoding: .utf8) ?? "") 34 | } 35 | let completionResponse = try JSONDecoder().decode(CompletionResponse.self, from: data) 36 | guard completionResponse.choices.count > 0 else { 37 | throw Errors.noChoices 38 | } 39 | return completionResponse.choices[0].text 40 | } 41 | 42 | public func completeStreaming(_ completionRequest: CompletionRequest) throws -> StreamingCompletion { 43 | var cr = completionRequest 44 | cr.stream = true 45 | 46 | let request = try createTextRequest(completionRequest: cr) 47 | let src = EventSource(urlRequest: request) 48 | let completion = StreamingCompletion() 49 | src.onComplete { statusCode, reconnect, error in 50 | DispatchQueue.main.async { 51 | if let statusCode, statusCode / 100 == 2 { 52 | completion.status = .complete 53 | } 54 | } 55 | } 56 | src.onMessage { id, event, data in 57 | guard let data else { return } 58 | let textOpt = decodeStreamingResponse(jsonStr: data) 59 | DispatchQueue.main.async { 60 | if let textOpt { 61 | completion.text += textOpt 62 | } 63 | } 64 | } 65 | src.connect() 66 | return completion 67 | } 68 | 69 | private func decodeStreamingResponse(jsonStr: String) -> String? { 70 | guard let json = try? JSONDecoder().decode(CompletionResponse.self, from: Data(jsonStr.utf8)) else { 71 | return nil 72 | } 73 | return json.choices.first?.text 74 | } 75 | 76 | private func createTextRequest(completionRequest: CompletionRequest) throws -> URLRequest { 77 | let url = URL(string: "https://api.openai.com/v1/completions")! 78 | var request = URLRequest(url: url) 79 | request.httpMethod = "POST" 80 | request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization") 81 | request.setValue("application/json", forHTTPHeaderField: "Content-Type") 82 | if let orgId { 83 | request.setValue(orgId, forHTTPHeaderField: "OpenAI-Organization") 84 | } 85 | request.httpBody = try JSONEncoder().encode(completionRequest) 86 | return request 87 | } 88 | } 89 | 90 | public class StreamingCompletion: ObservableObject { 91 | public enum Status: Equatable { 92 | case loading 93 | case complete 94 | case error 95 | } 96 | @Published public var status = Status.loading 97 | @Published public var text: String = "" 98 | 99 | init() {} 100 | } 101 | -------------------------------------------------------------------------------- /Sources/OpenAIStreamingCompletions/OpenAI+ChatCompletion.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension OpenAIAPI { 4 | public struct Message: Equatable, Codable, Hashable { 5 | public enum Role: String, Equatable, Codable, Hashable { 6 | case system 7 | case user 8 | case assistant 9 | } 10 | 11 | public var role: Role 12 | public var content: String 13 | 14 | public init(role: Role, content: String) { 15 | self.role = role 16 | self.content = content 17 | } 18 | } 19 | 20 | public struct ChatCompletionRequest: Codable { 21 | var messages: [Message] 22 | var model: String 23 | var max_tokens: Int = 1500 24 | var temperature: Double = 0.2 25 | var stream = false 26 | var stop: [String]? 27 | 28 | public init(messages: [Message], model: String = "gpt-3.5-turbo", max_tokens: Int = 1500, temperature: Double = 0.2, stop: [String]? = nil) { 29 | self.messages = messages 30 | self.model = model 31 | self.max_tokens = max_tokens 32 | self.temperature = temperature 33 | self.stop = stop 34 | } 35 | } 36 | 37 | // MARK: - Plain completion 38 | 39 | struct ChatCompletionResponse: Codable { 40 | struct Choice: Codable { 41 | var message: Message 42 | } 43 | var choices: [Choice] 44 | } 45 | 46 | public func completeChat(_ completionRequest: ChatCompletionRequest) async throws -> String { 47 | let request = try createChatRequest(completionRequest: completionRequest) 48 | let (data, response) = try await URLSession.shared.data(for: request) 49 | guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else { 50 | throw Errors.invalidResponse(String(data: data, encoding: .utf8) ?? "") 51 | } 52 | let completionResponse = try JSONDecoder().decode(ChatCompletionResponse.self, from: data) 53 | guard completionResponse.choices.count > 0 else { 54 | throw Errors.noChoices 55 | } 56 | return completionResponse.choices[0].message.content 57 | } 58 | 59 | // MARK: - Streaming completion 60 | 61 | public func completeChatStreaming(_ completionRequest: ChatCompletionRequest) throws -> AsyncStream { 62 | var cr = completionRequest 63 | cr.stream = true 64 | let request = try createChatRequest(completionRequest: cr) 65 | 66 | return AsyncStream { continuation in 67 | let src = EventSource(urlRequest: request) 68 | 69 | var message = Message(role: .assistant, content: "") 70 | 71 | src.onComplete { statusCode, reconnect, error in 72 | continuation.finish() 73 | } 74 | src.onMessage { id, event, data in 75 | guard let data, data != "[DONE]" else { return } 76 | do { 77 | let decoded = try JSONDecoder().decode(ChatCompletionStreamingResponse.self, from: Data(data.utf8)) 78 | if let delta = decoded.choices.first?.delta { 79 | message.role = delta.role ?? message.role 80 | message.content += delta.content ?? "" 81 | continuation.yield(message) 82 | } 83 | } catch { 84 | print("Chat completion error: \(error)") 85 | } 86 | } 87 | src.connect() 88 | } 89 | } 90 | 91 | public func completeChatStreamingWithObservableObject(_ completionRequest: ChatCompletionRequest) throws -> StreamingCompletion { 92 | let completion = StreamingCompletion() 93 | Task { 94 | do { 95 | for await message in try self.completeChatStreaming(completionRequest) { 96 | DispatchQueue.main.async { 97 | completion.text = message.content 98 | } 99 | } 100 | DispatchQueue.main.async { 101 | completion.status = .complete 102 | } 103 | } catch { 104 | DispatchQueue.main.async { 105 | completion.status = .error 106 | } 107 | } 108 | } 109 | return completion 110 | } 111 | 112 | private struct ChatCompletionStreamingResponse: Codable { 113 | struct Choice: Codable { 114 | struct MessageDelta: Codable { 115 | var role: Message.Role? 116 | var content: String? 117 | } 118 | var delta: MessageDelta 119 | } 120 | var choices: [Choice] 121 | } 122 | 123 | private func decodeChatStreamingResponse(jsonStr: String) -> String? { 124 | guard let json = try? JSONDecoder().decode(ChatCompletionStreamingResponse.self, from: Data(jsonStr.utf8)) else { 125 | return nil 126 | } 127 | return json.choices.first?.delta.content 128 | } 129 | 130 | private func createChatRequest(completionRequest: ChatCompletionRequest) throws -> URLRequest { 131 | let url = URL(string: "https://api.openai.com/v1/chat/completions")! 132 | var request = URLRequest(url: url) 133 | request.httpMethod = "POST" 134 | request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization") 135 | request.setValue("application/json", forHTTPHeaderField: "Content-Type") 136 | if let orgId { 137 | request.setValue(orgId, forHTTPHeaderField: "OpenAI-Organization") 138 | } 139 | request.httpBody = try JSONEncoder().encode(completionRequest) 140 | return request 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /Sources/OpenAIStreamingCompletions/EventSource/EventSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EventSource.swift 3 | // EventSource 4 | // 5 | // Created by Andres on 2/13/15. 6 | // Copyright (c) 2015 Inaka. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public enum EventSourceState { 12 | case connecting 13 | case open 14 | case closed 15 | } 16 | 17 | public protocol EventSourceProtocol { 18 | var headers: [String: String] { get } 19 | 20 | /// RetryTime: This can be changed remotly if the server sends an event `retry:` 21 | var retryTime: Int { get } 22 | 23 | /// URL where EventSource will listen for events. 24 | var url: URL { get } 25 | 26 | /// The last event id received from server. This id is neccesary to keep track of the last event-id received to avoid 27 | /// receiving duplicate events after a reconnection. 28 | var lastEventId: String? { get } 29 | 30 | /// Current state of EventSource 31 | var readyState: EventSourceState { get } 32 | 33 | /// Method used to connect to server. It can receive an optional lastEventId indicating the Last-Event-ID 34 | /// 35 | /// - Parameter lastEventId: optional value that is going to be added on the request header to server. 36 | func connect(lastEventId: String?) 37 | 38 | /// Method used to disconnect from server. 39 | func disconnect() 40 | 41 | /// Returns the list of event names that we are currently listening for. 42 | /// 43 | /// - Returns: List of event names. 44 | func events() -> [String] 45 | 46 | /// Callback called when EventSource has successfully connected to the server. 47 | /// 48 | /// - Parameter onOpenCallback: callback 49 | func onOpen(_ onOpenCallback: @escaping (() -> Void)) 50 | 51 | /// Callback called once EventSource has disconnected from server. This can happen for multiple reasons. 52 | /// The server could have requested the disconnection or maybe a network layer error, wrong URL or any other 53 | /// error. The callback receives as parameters the status code of the disconnection, if we should reconnect or not 54 | /// following event source rules and finally the network layer error if any. All this information is more than 55 | /// enought for you to take a decition if you should reconnect or not. 56 | /// - Parameter onOpenCallback: callback 57 | func onComplete(_ onComplete: @escaping ((Int?, Bool?, NSError?) -> Void)) 58 | 59 | /// This callback is called everytime an event with name "message" or no name is received. 60 | func onMessage(_ onMessageCallback: @escaping ((_ id: String?, _ event: String?, _ data: String?) -> Void)) 61 | 62 | /// Add an event handler for an specific event name. 63 | /// 64 | /// - Parameters: 65 | /// - event: name of the event to receive 66 | /// - handler: this handler will be called everytime an event is received with this event-name 67 | func addEventListener(_ event: String, 68 | handler: @escaping ((_ id: String?, _ event: String?, _ data: String?) -> Void)) 69 | 70 | /// Remove an event handler for the event-name 71 | /// 72 | /// - Parameter event: name of the listener to be remove from event source. 73 | func removeEventListener(_ event: String) 74 | } 75 | 76 | open class EventSource: NSObject, EventSourceProtocol, URLSessionDataDelegate { 77 | static let DefaultRetryTime = 3000 78 | 79 | public let urlRequest: URLRequest 80 | public var url: URL { urlRequest.url! } 81 | private(set) public var lastEventId: String? 82 | private(set) public var retryTime = EventSource.DefaultRetryTime 83 | private(set) public var headers: [String: String] 84 | private(set) public var readyState: EventSourceState 85 | 86 | private var onOpenCallback: (() -> Void)? 87 | private var onComplete: ((Int?, Bool?, NSError?) -> Void)? 88 | private var onMessageCallback: ((_ id: String?, _ event: String?, _ data: String?) -> Void)? 89 | private var eventListeners: [String: (_ id: String?, _ event: String?, _ data: String?) -> Void] = [:] 90 | 91 | private var eventStreamParser: EventStreamParser? 92 | private var operationQueue: OperationQueue 93 | private var mainQueue = DispatchQueue.main 94 | private var urlSession: URLSession? 95 | 96 | public init( 97 | urlRequest: URLRequest 98 | ) { 99 | self.urlRequest = urlRequest 100 | self.headers = urlRequest.allHTTPHeaderFields ?? [:] 101 | 102 | readyState = EventSourceState.closed 103 | operationQueue = OperationQueue() 104 | operationQueue.maxConcurrentOperationCount = 1 105 | 106 | super.init() 107 | } 108 | 109 | public func connect(lastEventId: String? = nil) { 110 | eventStreamParser = EventStreamParser() 111 | readyState = .connecting 112 | 113 | let configuration = sessionConfiguration(lastEventId: lastEventId) 114 | urlSession = URLSession(configuration: configuration, delegate: self, delegateQueue: operationQueue) 115 | urlSession?.dataTask(with: urlRequest).resume() 116 | } 117 | 118 | public func disconnect() { 119 | readyState = .closed 120 | urlSession?.invalidateAndCancel() 121 | } 122 | 123 | public func onOpen(_ onOpenCallback: @escaping (() -> Void)) { 124 | self.onOpenCallback = onOpenCallback 125 | } 126 | 127 | public func onComplete(_ onComplete: @escaping ((Int?, Bool?, NSError?) -> Void)) { 128 | self.onComplete = onComplete 129 | } 130 | 131 | public func onMessage(_ onMessageCallback: @escaping ((_ id: String?, _ event: String?, _ data: String?) -> Void)) { 132 | self.onMessageCallback = onMessageCallback 133 | } 134 | 135 | public func addEventListener(_ event: String, 136 | handler: @escaping ((_ id: String?, _ event: String?, _ data: String?) -> Void)) { 137 | eventListeners[event] = handler 138 | } 139 | 140 | public func removeEventListener(_ event: String) { 141 | eventListeners.removeValue(forKey: event) 142 | } 143 | 144 | public func events() -> [String] { 145 | return Array(eventListeners.keys) 146 | } 147 | 148 | open func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { 149 | 150 | if readyState != .open { 151 | return 152 | } 153 | 154 | if let events = eventStreamParser?.append(data: data) { 155 | notifyReceivedEvents(events) 156 | } 157 | } 158 | 159 | open func urlSession(_ session: URLSession, 160 | dataTask: URLSessionDataTask, 161 | didReceive response: URLResponse, 162 | completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) { 163 | 164 | completionHandler(URLSession.ResponseDisposition.allow) 165 | 166 | readyState = .open 167 | mainQueue.async { [weak self] in self?.onOpenCallback?() } 168 | } 169 | 170 | open func urlSession(_ session: URLSession, 171 | task: URLSessionTask, 172 | didCompleteWithError error: Error?) { 173 | 174 | guard let responseStatusCode = (task.response as? HTTPURLResponse)?.statusCode else { 175 | mainQueue.async { [weak self] in self?.onComplete?(nil, nil, error as NSError?) } 176 | return 177 | } 178 | 179 | let reconnect = shouldReconnect(statusCode: responseStatusCode) 180 | mainQueue.async { [weak self] in self?.onComplete?(responseStatusCode, reconnect, nil) } 181 | } 182 | 183 | open func urlSession(_ session: URLSession, 184 | task: URLSessionTask, 185 | willPerformHTTPRedirection response: HTTPURLResponse, 186 | newRequest request: URLRequest, 187 | completionHandler: @escaping (URLRequest?) -> Void) { 188 | 189 | var newRequest = request 190 | self.headers.forEach { newRequest.setValue($1, forHTTPHeaderField: $0) } 191 | completionHandler(newRequest) 192 | } 193 | } 194 | 195 | internal extension EventSource { 196 | 197 | func sessionConfiguration(lastEventId: String?) -> URLSessionConfiguration { 198 | 199 | var additionalHeaders = headers 200 | if let eventID = lastEventId { 201 | additionalHeaders["Last-Event-Id"] = eventID 202 | } 203 | 204 | additionalHeaders["Accept"] = "text/event-stream" 205 | additionalHeaders["Cache-Control"] = "no-cache" 206 | 207 | let sessionConfiguration = URLSessionConfiguration.default 208 | sessionConfiguration.timeoutIntervalForRequest = TimeInterval(INT_MAX) 209 | sessionConfiguration.timeoutIntervalForResource = TimeInterval(INT_MAX) 210 | sessionConfiguration.httpAdditionalHeaders = additionalHeaders 211 | 212 | return sessionConfiguration 213 | } 214 | 215 | func readyStateOpen() { 216 | readyState = .open 217 | } 218 | } 219 | 220 | private extension EventSource { 221 | 222 | func notifyReceivedEvents(_ events: [Event]) { 223 | 224 | for event in events { 225 | lastEventId = event.id 226 | retryTime = event.retryTime ?? EventSource.DefaultRetryTime 227 | 228 | if event.onlyRetryEvent == true { 229 | continue 230 | } 231 | 232 | if event.event == nil || event.event == "message" { 233 | mainQueue.async { [weak self] in self?.onMessageCallback?(event.id, "message", event.data) } 234 | } 235 | 236 | if let eventName = event.event, let eventHandler = eventListeners[eventName] { 237 | mainQueue.async { eventHandler(event.id, event.event, event.data) } 238 | } 239 | } 240 | } 241 | 242 | // Following "5 Processing model" from: 243 | // https://www.w3.org/TR/2009/WD-eventsource-20090421/#handler-eventsource-onerror 244 | func shouldReconnect(statusCode: Int) -> Bool { 245 | switch statusCode { 246 | case 200: 247 | return false 248 | case _ where statusCode > 200 && statusCode < 300: 249 | return true 250 | default: 251 | return false 252 | } 253 | } 254 | } 255 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Demo/Demo.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 56; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 5F43D6DA29A87180002752A0 /* DemoApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F43D6D929A87180002752A0 /* DemoApp.swift */; }; 11 | 5F43D6DC29A87180002752A0 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F43D6DB29A87180002752A0 /* ContentView.swift */; }; 12 | 5F43D6DE29A87181002752A0 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5F43D6DD29A87181002752A0 /* Assets.xcassets */; }; 13 | 5F43D6E129A87181002752A0 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5F43D6E029A87181002752A0 /* Preview Assets.xcassets */; }; 14 | 5F43D6F329A8730F002752A0 /* OpenAIStreamingCompletions in Frameworks */ = {isa = PBXBuildFile; productRef = 5F43D6F229A8730F002752A0 /* OpenAIStreamingCompletions */; }; 15 | /* End PBXBuildFile section */ 16 | 17 | /* Begin PBXFileReference section */ 18 | 5F43D6D629A87180002752A0 /* Demo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Demo.app; sourceTree = BUILT_PRODUCTS_DIR; }; 19 | 5F43D6D929A87180002752A0 /* DemoApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DemoApp.swift; sourceTree = ""; }; 20 | 5F43D6DB29A87180002752A0 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 21 | 5F43D6DD29A87181002752A0 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 22 | 5F43D6E029A87181002752A0 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 23 | 5F43D6ED29A87266002752A0 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; 24 | 5F43D6EF29A87302002752A0 /* openai-streaming-completions-swift */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = "openai-streaming-completions-swift"; path = ..; sourceTree = ""; }; 25 | /* End PBXFileReference section */ 26 | 27 | /* Begin PBXFrameworksBuildPhase section */ 28 | 5F43D6D329A87180002752A0 /* Frameworks */ = { 29 | isa = PBXFrameworksBuildPhase; 30 | buildActionMask = 2147483647; 31 | files = ( 32 | 5F43D6F329A8730F002752A0 /* OpenAIStreamingCompletions in Frameworks */, 33 | ); 34 | runOnlyForDeploymentPostprocessing = 0; 35 | }; 36 | /* End PBXFrameworksBuildPhase section */ 37 | 38 | /* Begin PBXGroup section */ 39 | 5F43D6CD29A87180002752A0 = { 40 | isa = PBXGroup; 41 | children = ( 42 | 5F43D6EE29A87302002752A0 /* Packages */, 43 | 5F43D6D829A87180002752A0 /* Demo */, 44 | 5F43D6D729A87180002752A0 /* Products */, 45 | 5F43D6EA29A87256002752A0 /* Frameworks */, 46 | ); 47 | sourceTree = ""; 48 | }; 49 | 5F43D6D729A87180002752A0 /* Products */ = { 50 | isa = PBXGroup; 51 | children = ( 52 | 5F43D6D629A87180002752A0 /* Demo.app */, 53 | ); 54 | name = Products; 55 | sourceTree = ""; 56 | }; 57 | 5F43D6D829A87180002752A0 /* Demo */ = { 58 | isa = PBXGroup; 59 | children = ( 60 | 5F43D6ED29A87266002752A0 /* Info.plist */, 61 | 5F43D6D929A87180002752A0 /* DemoApp.swift */, 62 | 5F43D6DB29A87180002752A0 /* ContentView.swift */, 63 | 5F43D6DD29A87181002752A0 /* Assets.xcassets */, 64 | 5F43D6DF29A87181002752A0 /* Preview Content */, 65 | ); 66 | path = Demo; 67 | sourceTree = ""; 68 | }; 69 | 5F43D6DF29A87181002752A0 /* Preview Content */ = { 70 | isa = PBXGroup; 71 | children = ( 72 | 5F43D6E029A87181002752A0 /* Preview Assets.xcassets */, 73 | ); 74 | path = "Preview Content"; 75 | sourceTree = ""; 76 | }; 77 | 5F43D6EA29A87256002752A0 /* Frameworks */ = { 78 | isa = PBXGroup; 79 | children = ( 80 | ); 81 | name = Frameworks; 82 | sourceTree = ""; 83 | }; 84 | 5F43D6EE29A87302002752A0 /* Packages */ = { 85 | isa = PBXGroup; 86 | children = ( 87 | 5F43D6EF29A87302002752A0 /* openai-streaming-completions-swift */, 88 | ); 89 | name = Packages; 90 | sourceTree = ""; 91 | }; 92 | /* End PBXGroup section */ 93 | 94 | /* Begin PBXNativeTarget section */ 95 | 5F43D6D529A87180002752A0 /* Demo */ = { 96 | isa = PBXNativeTarget; 97 | buildConfigurationList = 5F43D6E429A87181002752A0 /* Build configuration list for PBXNativeTarget "Demo" */; 98 | buildPhases = ( 99 | 5F43D6D229A87180002752A0 /* Sources */, 100 | 5F43D6D329A87180002752A0 /* Frameworks */, 101 | 5F43D6D429A87180002752A0 /* Resources */, 102 | ); 103 | buildRules = ( 104 | ); 105 | dependencies = ( 106 | 5F43D6F129A8730C002752A0 /* PBXTargetDependency */, 107 | ); 108 | name = Demo; 109 | packageProductDependencies = ( 110 | 5F43D6F229A8730F002752A0 /* OpenAIStreamingCompletions */, 111 | ); 112 | productName = Demo; 113 | productReference = 5F43D6D629A87180002752A0 /* Demo.app */; 114 | productType = "com.apple.product-type.application"; 115 | }; 116 | /* End PBXNativeTarget section */ 117 | 118 | /* Begin PBXProject section */ 119 | 5F43D6CE29A87180002752A0 /* Project object */ = { 120 | isa = PBXProject; 121 | attributes = { 122 | BuildIndependentTargetsInParallel = 1; 123 | LastSwiftUpdateCheck = 1410; 124 | LastUpgradeCheck = 1410; 125 | TargetAttributes = { 126 | 5F43D6D529A87180002752A0 = { 127 | CreatedOnToolsVersion = 14.1; 128 | }; 129 | }; 130 | }; 131 | buildConfigurationList = 5F43D6D129A87180002752A0 /* Build configuration list for PBXProject "Demo" */; 132 | compatibilityVersion = "Xcode 14.0"; 133 | developmentRegion = en; 134 | hasScannedForEncodings = 0; 135 | knownRegions = ( 136 | en, 137 | Base, 138 | ); 139 | mainGroup = 5F43D6CD29A87180002752A0; 140 | productRefGroup = 5F43D6D729A87180002752A0 /* Products */; 141 | projectDirPath = ""; 142 | projectRoot = ""; 143 | targets = ( 144 | 5F43D6D529A87180002752A0 /* Demo */, 145 | ); 146 | }; 147 | /* End PBXProject section */ 148 | 149 | /* Begin PBXResourcesBuildPhase section */ 150 | 5F43D6D429A87180002752A0 /* Resources */ = { 151 | isa = PBXResourcesBuildPhase; 152 | buildActionMask = 2147483647; 153 | files = ( 154 | 5F43D6E129A87181002752A0 /* Preview Assets.xcassets in Resources */, 155 | 5F43D6DE29A87181002752A0 /* Assets.xcassets in Resources */, 156 | ); 157 | runOnlyForDeploymentPostprocessing = 0; 158 | }; 159 | /* End PBXResourcesBuildPhase section */ 160 | 161 | /* Begin PBXSourcesBuildPhase section */ 162 | 5F43D6D229A87180002752A0 /* Sources */ = { 163 | isa = PBXSourcesBuildPhase; 164 | buildActionMask = 2147483647; 165 | files = ( 166 | 5F43D6DC29A87180002752A0 /* ContentView.swift in Sources */, 167 | 5F43D6DA29A87180002752A0 /* DemoApp.swift in Sources */, 168 | ); 169 | runOnlyForDeploymentPostprocessing = 0; 170 | }; 171 | /* End PBXSourcesBuildPhase section */ 172 | 173 | /* Begin PBXTargetDependency section */ 174 | 5F43D6F129A8730C002752A0 /* PBXTargetDependency */ = { 175 | isa = PBXTargetDependency; 176 | productRef = 5F43D6F029A8730C002752A0 /* OpenAIStreamingCompletions */; 177 | }; 178 | /* End PBXTargetDependency section */ 179 | 180 | /* Begin XCBuildConfiguration section */ 181 | 5F43D6E229A87181002752A0 /* Debug */ = { 182 | isa = XCBuildConfiguration; 183 | buildSettings = { 184 | ALWAYS_SEARCH_USER_PATHS = NO; 185 | CLANG_ANALYZER_NONNULL = YES; 186 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 187 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 188 | CLANG_ENABLE_MODULES = YES; 189 | CLANG_ENABLE_OBJC_ARC = YES; 190 | CLANG_ENABLE_OBJC_WEAK = YES; 191 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 192 | CLANG_WARN_BOOL_CONVERSION = YES; 193 | CLANG_WARN_COMMA = YES; 194 | CLANG_WARN_CONSTANT_CONVERSION = YES; 195 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 196 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 197 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 198 | CLANG_WARN_EMPTY_BODY = YES; 199 | CLANG_WARN_ENUM_CONVERSION = YES; 200 | CLANG_WARN_INFINITE_RECURSION = YES; 201 | CLANG_WARN_INT_CONVERSION = YES; 202 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 203 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 204 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 205 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 206 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 207 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 208 | CLANG_WARN_STRICT_PROTOTYPES = YES; 209 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 210 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 211 | CLANG_WARN_UNREACHABLE_CODE = YES; 212 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 213 | COPY_PHASE_STRIP = NO; 214 | DEBUG_INFORMATION_FORMAT = dwarf; 215 | ENABLE_STRICT_OBJC_MSGSEND = YES; 216 | ENABLE_TESTABILITY = YES; 217 | GCC_C_LANGUAGE_STANDARD = gnu11; 218 | GCC_DYNAMIC_NO_PIC = NO; 219 | GCC_NO_COMMON_BLOCKS = YES; 220 | GCC_OPTIMIZATION_LEVEL = 0; 221 | GCC_PREPROCESSOR_DEFINITIONS = ( 222 | "DEBUG=1", 223 | "$(inherited)", 224 | ); 225 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 226 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 227 | GCC_WARN_UNDECLARED_SELECTOR = YES; 228 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 229 | GCC_WARN_UNUSED_FUNCTION = YES; 230 | GCC_WARN_UNUSED_VARIABLE = YES; 231 | IPHONEOS_DEPLOYMENT_TARGET = 16.1; 232 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 233 | MTL_FAST_MATH = YES; 234 | ONLY_ACTIVE_ARCH = YES; 235 | SDKROOT = iphoneos; 236 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 237 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 238 | }; 239 | name = Debug; 240 | }; 241 | 5F43D6E329A87181002752A0 /* Release */ = { 242 | isa = XCBuildConfiguration; 243 | buildSettings = { 244 | ALWAYS_SEARCH_USER_PATHS = NO; 245 | CLANG_ANALYZER_NONNULL = YES; 246 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 247 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 248 | CLANG_ENABLE_MODULES = YES; 249 | CLANG_ENABLE_OBJC_ARC = YES; 250 | CLANG_ENABLE_OBJC_WEAK = YES; 251 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 252 | CLANG_WARN_BOOL_CONVERSION = YES; 253 | CLANG_WARN_COMMA = YES; 254 | CLANG_WARN_CONSTANT_CONVERSION = YES; 255 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 256 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 257 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 258 | CLANG_WARN_EMPTY_BODY = YES; 259 | CLANG_WARN_ENUM_CONVERSION = YES; 260 | CLANG_WARN_INFINITE_RECURSION = YES; 261 | CLANG_WARN_INT_CONVERSION = YES; 262 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 263 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 264 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 265 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 266 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 267 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 268 | CLANG_WARN_STRICT_PROTOTYPES = YES; 269 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 270 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 271 | CLANG_WARN_UNREACHABLE_CODE = YES; 272 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 273 | COPY_PHASE_STRIP = NO; 274 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 275 | ENABLE_NS_ASSERTIONS = NO; 276 | ENABLE_STRICT_OBJC_MSGSEND = YES; 277 | GCC_C_LANGUAGE_STANDARD = gnu11; 278 | GCC_NO_COMMON_BLOCKS = YES; 279 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 280 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 281 | GCC_WARN_UNDECLARED_SELECTOR = YES; 282 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 283 | GCC_WARN_UNUSED_FUNCTION = YES; 284 | GCC_WARN_UNUSED_VARIABLE = YES; 285 | IPHONEOS_DEPLOYMENT_TARGET = 16.1; 286 | MTL_ENABLE_DEBUG_INFO = NO; 287 | MTL_FAST_MATH = YES; 288 | SDKROOT = iphoneos; 289 | SWIFT_COMPILATION_MODE = wholemodule; 290 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 291 | VALIDATE_PRODUCT = YES; 292 | }; 293 | name = Release; 294 | }; 295 | 5F43D6E529A87181002752A0 /* Debug */ = { 296 | isa = XCBuildConfiguration; 297 | buildSettings = { 298 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 299 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 300 | CODE_SIGN_STYLE = Automatic; 301 | CURRENT_PROJECT_VERSION = 1; 302 | DEVELOPMENT_ASSET_PATHS = "\"Demo/Preview Content\""; 303 | DEVELOPMENT_TEAM = VPN8ZW6EAC; 304 | ENABLE_PREVIEWS = YES; 305 | GENERATE_INFOPLIST_FILE = YES; 306 | INFOPLIST_FILE = Demo/Info.plist; 307 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 308 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 309 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 310 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 311 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 312 | LD_RUNPATH_SEARCH_PATHS = ( 313 | "$(inherited)", 314 | "@executable_path/Frameworks", 315 | ); 316 | MARKETING_VERSION = 1.0; 317 | PRODUCT_BUNDLE_IDENTIFIER = com.nateparrott.Demo; 318 | PRODUCT_NAME = "$(TARGET_NAME)"; 319 | SWIFT_EMIT_LOC_STRINGS = YES; 320 | SWIFT_VERSION = 5.0; 321 | TARGETED_DEVICE_FAMILY = "1,2"; 322 | }; 323 | name = Debug; 324 | }; 325 | 5F43D6E629A87181002752A0 /* Release */ = { 326 | isa = XCBuildConfiguration; 327 | buildSettings = { 328 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 329 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 330 | CODE_SIGN_STYLE = Automatic; 331 | CURRENT_PROJECT_VERSION = 1; 332 | DEVELOPMENT_ASSET_PATHS = "\"Demo/Preview Content\""; 333 | DEVELOPMENT_TEAM = VPN8ZW6EAC; 334 | ENABLE_PREVIEWS = YES; 335 | GENERATE_INFOPLIST_FILE = YES; 336 | INFOPLIST_FILE = Demo/Info.plist; 337 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 338 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 339 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 340 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 341 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 342 | LD_RUNPATH_SEARCH_PATHS = ( 343 | "$(inherited)", 344 | "@executable_path/Frameworks", 345 | ); 346 | MARKETING_VERSION = 1.0; 347 | PRODUCT_BUNDLE_IDENTIFIER = com.nateparrott.Demo; 348 | PRODUCT_NAME = "$(TARGET_NAME)"; 349 | SWIFT_EMIT_LOC_STRINGS = YES; 350 | SWIFT_VERSION = 5.0; 351 | TARGETED_DEVICE_FAMILY = "1,2"; 352 | }; 353 | name = Release; 354 | }; 355 | /* End XCBuildConfiguration section */ 356 | 357 | /* Begin XCConfigurationList section */ 358 | 5F43D6D129A87180002752A0 /* Build configuration list for PBXProject "Demo" */ = { 359 | isa = XCConfigurationList; 360 | buildConfigurations = ( 361 | 5F43D6E229A87181002752A0 /* Debug */, 362 | 5F43D6E329A87181002752A0 /* Release */, 363 | ); 364 | defaultConfigurationIsVisible = 0; 365 | defaultConfigurationName = Release; 366 | }; 367 | 5F43D6E429A87181002752A0 /* Build configuration list for PBXNativeTarget "Demo" */ = { 368 | isa = XCConfigurationList; 369 | buildConfigurations = ( 370 | 5F43D6E529A87181002752A0 /* Debug */, 371 | 5F43D6E629A87181002752A0 /* Release */, 372 | ); 373 | defaultConfigurationIsVisible = 0; 374 | defaultConfigurationName = Release; 375 | }; 376 | /* End XCConfigurationList section */ 377 | 378 | /* Begin XCSwiftPackageProductDependency section */ 379 | 5F43D6F029A8730C002752A0 /* OpenAIStreamingCompletions */ = { 380 | isa = XCSwiftPackageProductDependency; 381 | productName = OpenAIStreamingCompletions; 382 | }; 383 | 5F43D6F229A8730F002752A0 /* OpenAIStreamingCompletions */ = { 384 | isa = XCSwiftPackageProductDependency; 385 | productName = OpenAIStreamingCompletions; 386 | }; 387 | /* End XCSwiftPackageProductDependency section */ 388 | }; 389 | rootObject = 5F43D6CE29A87180002752A0 /* Project object */; 390 | } 391 | --------------------------------------------------------------------------------