├── 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 |
--------------------------------------------------------------------------------