├── .gitignore
├── .spi.yml
├── .swiftpm
└── xcode
│ ├── package.xcworkspace
│ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
│ └── xcshareddata
│ └── xcschemes
│ ├── ChatGPT.xcscheme
│ └── GPT.xcscheme
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── LICENSE
├── Package.resolved
├── Package.swift
├── README.md
└── Sources
├── Base
├── API.swift
└── APIClientRequestHandler.swift
├── ChatGPT
├── ChatGPT+StreamedAnswer.swift
├── ChatGPT.swift
└── Models
│ ├── ChatGPTModel.swift
│ ├── ChatMessage.swift
│ ├── ChatMessageRole.swift
│ ├── ChatRequest.swift
│ ├── ChatResponse.swift
│ ├── ChatStreamedMessage.swift
│ └── ChatStreamedResponse.swift
├── GPT
├── GPT+StreamedAnswer.swift
├── GPT.swift
└── Models
│ ├── CompletionRequest.swift
│ ├── CompletionResponse.swift
│ ├── CompletionStreamedResponse.swift
│ └── GPTModel.swift
├── GPTSwiftSharedTypes
├── GPTSwiftError.swift
├── OpenAIError.swift
└── URLRequest+curl.swift
└── OpenAI
├── Models
└── ModelList.swift
└── OpenAI.swift
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /.build
3 | /Packages
4 | /*.xcodeproj
5 | xcuserdata/
6 | DerivedData/
7 | .swiftpm/config/registries.json
8 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
9 | .netrc
10 |
--------------------------------------------------------------------------------
/.spi.yml:
--------------------------------------------------------------------------------
1 | version: 1
2 | builder:
3 | configs:
4 | - documentation_targets: [GPTSwift]
5 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/xcshareddata/xcschemes/ChatGPT.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
31 |
32 |
42 |
43 |
49 |
50 |
56 |
57 |
58 |
59 |
61 |
62 |
65 |
66 |
67 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/xcshareddata/xcschemes/GPT.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
31 |
32 |
42 |
43 |
49 |
50 |
56 |
57 |
58 |
59 |
61 |
62 |
65 |
66 |
67 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## 3.0.0
4 |
5 | > **Note**
6 | > Fun Fact: Parts of the documentation for this update have been written by GPT4! I have proof-read them to make sure everything is fine but GPT4 just did it perfectly.
7 |
8 | ### Added
9 |
10 | - Added GPT4 models (also including stable and large context versions) to `ChatGPT`. If you have access, enjoy!
11 | - Added `GPT`, which is a wrapper around the completion API for GPT3. Also supports streaming the answers.
12 | - Added `OpenAI`, which is a wrapper around two general OpenAI endpoints that list available models.
13 | - Added more robust and convenient error handling. All errors are now exposed through [`GPTSwiftError`](https://github.com/SwiftedMind/GPTSwift/blob/main/Sources/GPTSwiftSharedTypes/GPTSwiftError.swift).
14 | - OpenAI error messages are now also passed through, which is useful to debug invalid requests (like out-of-bounds values).
15 | - Added option to configure underlying URLSession in the initializers of `ChatGPT`, `GPT` and `OpenAI`.
16 | - Added option to set the default model to use for instances of `ChatGPT`, `GPT` and `OpenAI`. You can set that in the initializer and then, if needed, override it in the individual methods.
17 | - Added convenience methods to initialize requests and messages, for example `ChatRequest.gpt3(_:)` or `ChatMessage.system(_:)`.
18 | - Added `curl(for:pretty:formatOutput:)` methods to `ChatGPT` and `GPT` that turns requests into a usable `curl` prompt that you can paste into a terminal to manually call the OpenAI endpoints.
19 | - `OpenAI` also has these convenience methods: `curlForAvailableModels(pretty:formatOutput:)` and `curlForModel(withId:pretty:formatOutput:)`
20 |
21 | ### Changed
22 |
23 | - Renamed `ChatGPTSwift` to `ChatGPT`.
24 | - `ChatGPT.ask(_:)` and `ChatGPT.ask(messages:)` have been made easier to use. They now return a String as the answer, instead of a complex object.
25 | - The intention behind this is to make basic usage of the framework as easy as possible while still allowing full control through `ChatGPT.ask(request:)`
26 | - `ChatGPT`, `GPT` and `OpenAI` now each have their own product to reduce namespace pollution. Simply import whichever you are using.
27 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | We as members, contributors, and leaders pledge to make participation in our
6 | community a harassment-free experience for everyone, regardless of age, body
7 | size, visible or invisible disability, ethnicity, sex characteristics, gender
8 | identity and expression, level of experience, education, socio-economic status,
9 | nationality, personal appearance, race, religion, or sexual identity
10 | and orientation.
11 |
12 | We pledge to act and interact in ways that contribute to an open, welcoming,
13 | diverse, inclusive, and healthy community.
14 |
15 | ## Our Standards
16 |
17 | Examples of behavior that contributes to a positive environment for our
18 | community include:
19 |
20 | * Demonstrating empathy and kindness toward other people
21 | * Being respectful of differing opinions, viewpoints, and experiences
22 | * Giving and gracefully accepting constructive feedback
23 | * Accepting responsibility and apologizing to those affected by our mistakes,
24 | and learning from the experience
25 | * Focusing on what is best not just for us as individuals, but for the
26 | overall community
27 |
28 | Examples of unacceptable behavior include:
29 |
30 | * The use of sexualized language or imagery, and sexual attention or
31 | advances of any kind
32 | * Trolling, insulting or derogatory comments, and personal or political attacks
33 | * Public or private harassment
34 | * Publishing others' private information, such as a physical or email
35 | address, without their explicit permission
36 | * Other conduct which could reasonably be considered inappropriate in a
37 | professional setting
38 |
39 | ## Enforcement Responsibilities
40 |
41 | Community leaders are responsible for clarifying and enforcing our standards of
42 | acceptable behavior and will take appropriate and fair corrective action in
43 | response to any behavior that they deem inappropriate, threatening, offensive,
44 | or harmful.
45 |
46 | Community leaders have the right and responsibility to remove, edit, or reject
47 | comments, commits, code, wiki edits, issues, and other contributions that are
48 | not aligned to this Code of Conduct, and will communicate reasons for moderation
49 | decisions when appropriate.
50 |
51 | ## Scope
52 |
53 | This Code of Conduct applies within all community spaces, and also applies when
54 | an individual is officially representing the community in public spaces.
55 | Examples of representing our community include using an official e-mail address,
56 | posting via an official social media account, or acting as an appointed
57 | representative at an online or offline event.
58 |
59 | ## Enforcement
60 |
61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
62 | reported to the community leaders responsible for enforcement at
63 | dennis@swiftedmind.com.
64 | All complaints will be reviewed and investigated promptly and fairly.
65 |
66 | All community leaders are obligated to respect the privacy and security of the
67 | reporter of any incident.
68 |
69 | ## Enforcement Guidelines
70 |
71 | Community leaders will follow these Community Impact Guidelines in determining
72 | the consequences for any action they deem in violation of this Code of Conduct:
73 |
74 | ### 1. Correction
75 |
76 | **Community Impact**: Use of inappropriate language or other behavior deemed
77 | unprofessional or unwelcome in the community.
78 |
79 | **Consequence**: A private, written warning from community leaders, providing
80 | clarity around the nature of the violation and an explanation of why the
81 | behavior was inappropriate. A public apology may be requested.
82 |
83 | ### 2. Warning
84 |
85 | **Community Impact**: A violation through a single incident or series
86 | of actions.
87 |
88 | **Consequence**: A warning with consequences for continued behavior. No
89 | interaction with the people involved, including unsolicited interaction with
90 | those enforcing the Code of Conduct, for a specified period of time. This
91 | includes avoiding interactions in community spaces as well as external channels
92 | like social media. Violating these terms may lead to a temporary or
93 | permanent ban.
94 |
95 | ### 3. Temporary Ban
96 |
97 | **Community Impact**: A serious violation of community standards, including
98 | sustained inappropriate behavior.
99 |
100 | **Consequence**: A temporary ban from any sort of interaction or public
101 | communication with the community for a specified period of time. No public or
102 | private interaction with the people involved, including unsolicited interaction
103 | with those enforcing the Code of Conduct, is allowed during this period.
104 | Violating these terms may lead to a permanent ban.
105 |
106 | ### 4. Permanent Ban
107 |
108 | **Community Impact**: Demonstrating a pattern of violation of community
109 | standards, including sustained inappropriate behavior, harassment of an
110 | individual, or aggression toward or disparagement of classes of individuals.
111 |
112 | **Consequence**: A permanent ban from any sort of public interaction within
113 | the community.
114 |
115 | ## Attribution
116 |
117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage],
118 | version 2.0, available at
119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
120 |
121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct
122 | enforcement ladder](https://github.com/mozilla/diversity).
123 |
124 | [homepage]: https://www.contributor-covenant.org
125 |
126 | For answers to common questions about this code of conduct, see the FAQ at
127 | https://www.contributor-covenant.org/faq. Translations are available at
128 | https://www.contributor-covenant.org/translations.
129 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Dennis Müller
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "pins" : [
3 | {
4 | "identity" : "get",
5 | "kind" : "remoteSourceControl",
6 | "location" : "https://github.com/kean/Get",
7 | "state" : {
8 | "revision" : "12830cc64f31789ae6f4352d2d51d03a25fc3741",
9 | "version" : "2.1.6"
10 | }
11 | }
12 | ],
13 | "version" : 2
14 | }
15 |
--------------------------------------------------------------------------------
/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: "GPTSwift",
8 | platforms: [
9 | .iOS(.v13),
10 | .macOS(.v10_15),
11 | .watchOS(.v8),
12 | .tvOS(.v15)
13 | ],
14 | products: [
15 | .library(
16 | name: "ChatGPT",
17 | targets: ["ChatGPT", "GPTSwiftSharedTypes"]
18 | ),
19 | .library(
20 | name: "GPT",
21 | targets: ["GPT", "GPTSwiftSharedTypes"]
22 | ),
23 | .library(
24 | name: "OpenAI",
25 | targets: ["OpenAI", "GPTSwiftSharedTypes"]
26 | )
27 | ],
28 | dependencies: [
29 | .package(url: "https://github.com/kean/Get", from: "2.1.6")
30 | ],
31 | targets: [
32 | .target(
33 | name: "Base",
34 | dependencies: [
35 | "GPTSwiftSharedTypes",
36 | .product(name: "Get", package: "Get")
37 | ]
38 | ),
39 | .target(
40 | name: "ChatGPT",
41 | dependencies: [
42 | "Base",
43 | "GPTSwiftSharedTypes",
44 | .product(name: "Get", package: "Get")
45 | ]
46 | ),
47 | .target(
48 | name: "GPT",
49 | dependencies: [
50 | "Base",
51 | "GPTSwiftSharedTypes",
52 | .product(name: "Get", package: "Get")
53 | ]
54 | ),
55 | .target(
56 | name: "OpenAI",
57 | dependencies: [
58 | "Base",
59 | "GPTSwiftSharedTypes",
60 | .product(name: "Get", package: "Get")
61 | ]
62 | ),
63 | .target(
64 | name: "GPTSwiftSharedTypes",
65 | dependencies: []
66 | )
67 | ]
68 | )
69 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # GPTSwift
4 | [](https://swiftpackageindex.com/SwiftedMind/GPTSwift)
5 | [](https://swiftpackageindex.com/SwiftedMind/GPTSwift)
6 | 
7 |
8 | GPTSwift is a lightweight and convenient wrapper around the OpenAI API. It is completely written in Swift.
9 |
10 | Using GPTSwift is easy:
11 |
12 | ```swift
13 | import ChatGPT
14 |
15 | let chatGPT = ChatGPT(apiKey: "YOUR_API_KEY", defaultModel: .gpt3)
16 | let answer = try await chatGPT.ask("What is the answer to life, the universe and everything in it?")
17 |
18 | // Stream the answer token by token
19 | var streamedAnswer = ""
20 | for try await nextWord in try await chatGPT.streamedAnswer.ask("Tell me a story about birds") {
21 | streamedAnswer += nextWord
22 | }
23 | ```
24 |
25 | An example project can be found here: [GPTPlayground](https://github.com/SwiftedMind/GPTPlayground/tree/main)
26 |
27 | ## Content
28 |
29 | - [**Getting Started**](#getting-started)
30 | - [Requirements](#requirements)
31 | - [Installation](#installation)
32 | - [**How to Use**](#how-to-use)
33 | - [Generating cURL Prompts](#generating-curl-prompts)
34 | - [License](#license)
35 |
36 | ## Getting Started
37 |
38 | ### Requirements
39 |
40 | GPTSwift supports iOS 15+, macOS 12+, watchOS 8+ and tvOS 15+.
41 |
42 | ### Installation
43 |
44 | The package is installed through the Swift Package Manager. Simply add the following line to your `Package.swift` dependencies:
45 |
46 | ```swift
47 | .package(url: "https://github.com/SwiftedMind/GPTSwift", from: "3.0.0")
48 | ```
49 |
50 | Alternatively, if you want to add the package to an Xcode project, go to `File` > `Add Packages...` and enter the URL "https://github.com/SwiftedMind/GPTSwift" into the search field at the top. GPTSwift should appear in the list. Select it and click "Add Package" in the bottom right.
51 |
52 | ## How to Use
53 |
54 | GPTSwift is just a lightweight wrapper around the API that abstracts away all the unnecessary details of calling endpoints and handling requests and responses.
55 |
56 | ### ChatGPT
57 |
58 | The `ChatGPT` target gives you access to the ChatGPT API (including the GPT4 models).
59 |
60 | The easiest way of using `ChatGPT` is by simply passing in a prompt string or an array of chat messages via the `ask(_:)` and `ask(messages:)` methods. These take care of turning the arguments into a request as well as parsing the response. All you get is the answer as a simple string, which is often all that's needed.
61 |
62 | ```swift
63 | import ChatGPT
64 |
65 | func askChatGPT() async throws {
66 | let chatGPT = ChatGPT(apiKey: "YOUR_API_KEY", defaultModel: .gpt3)
67 |
68 | // Basic query
69 | let firstResponse = try await chatGPT.ask("What is the answer to life, the universe and everything in it?")
70 | print(firstResponse)
71 |
72 | // Send multiple messages
73 | let secondResponse = try await chatGPT.ask(
74 | messages: [
75 | ChatMessage(role: .system, content: "You are a dog."),
76 | ChatMessage(role: .user, content: "Do you actually like playing fetch?")
77 | ],
78 | model: .gpt3.stableVersion() // Override default model, if needed
79 | )
80 | print(secondResponse)
81 | }
82 | ```
83 |
84 | However, if you need full control, you can also pass a `ChatRequest` to the `ask(request:)` method. With this, you can adjust all parameters and have access to the full response object, while everything is still fully type-safe.
85 |
86 | ```swift
87 | import ChatGPT
88 |
89 | func askChatGPT() async throws {
90 | let chatGPT = ChatGPT(apiKey: "YOUR_API_KEY", defaultModel: .gpt3)
91 |
92 | let fullRequest = ChatRequest.gpt3 { request in
93 | request.messages = [
94 | .init(role: .system, content: "You are the pilot of an alien UFO. Be creative."),
95 | .init(role: .user, content: "Where do you come from?")
96 | ]
97 | request.temperature = 0.8
98 | request.numberOfAnswers = 2
99 | }
100 |
101 | let response = try await chatGPT.ask(request: fullRequest)
102 | print(response.choices.map(\.message))
103 | }
104 | ```
105 |
106 | Finally, all of the above methods have a variant that lets you stream GPT's answer token by token, right as they are generated. The stream is provided via an `AsyncThrowingStream`. All you have to do is add a `streamedAnswer` before the call to `ask()`. For example:
107 |
108 | ```swift
109 | import ChatGPT
110 |
111 | // In your view model
112 | @Published var gptAnswer = ""
113 |
114 | func askChatGPT() async throws {
115 | let chatGPT = ChatGPT(apiKey: "YOUR_API_KEY")
116 |
117 | gptAnswer = ""
118 | for try await nextWord in try await chatGPT.streamedAnswer.ask("Tell me a funny story about birds") {
119 | gptAnswer += nextWord
120 | }
121 | }
122 | ```
123 |
124 | For more information about the ChatGPT API, you can look at OpenAI's documentation:
125 | - [ChatGPT API Introduction](https://platform.openai.com/docs/guides/chat/chat-completions-beta)
126 | - [ChatGPT API documentation](https://platform.openai.com/docs/api-reference/chat/create)
127 |
128 |
129 | ### GPT
130 |
131 | Like `ChatGPT`, `GPT` is a wrapper around the completion API. There is a basic `complete(_:)` method for convenient use, as well as a `complete(request:)` method that gives you full control.
132 |
133 | ```swift
134 | import GPT
135 |
136 | func askGPT() async throws {
137 | let gpt = GPT(apiKey: "YOUR_API_KEY", defaultModel: .davinci)
138 |
139 | let response = try await gpt.complete("What is the answer to life, the universe and everything in it?")
140 | print(response)
141 | }
142 | ```
143 |
144 | ```swift
145 | import GPT
146 |
147 | func askGPT() async throws {
148 | let gpt = GPT(apiKey: "YOUR_API_KEY", defaultModel: .davinci)
149 |
150 | let fullRequest = CompletionRequest.davinci(prompt: "Why is the sky blue?") { request in
151 | request.temperature = 0.8
152 | request.numberOfAnswers = 2
153 | }
154 |
155 | let response = try await gpt.complete(request: fullRequest)
156 | print(response.choices.map(\.text))
157 | }
158 | ```
159 |
160 | Additionally, just like `ChatGPT`, `GPT` also supports streaming answers:
161 |
162 | ```swift
163 | import GPT
164 |
165 | // In your view model
166 | @Published var gptAnswer = ""
167 |
168 | func askGPT() async throws {
169 | let gpt = GPT(apiKey: "YOUR_API_KEY")
170 |
171 | gptAnswer = ""
172 | for try await nextWord in try await gpt.streamedAnswer.complete("Tell me a funny story about birds") {
173 | gptAnswer += nextWord
174 | }
175 | }
176 | ```
177 |
178 | ### OpenAI
179 |
180 | Finally, you can access the available models through the `OpenAI` class.
181 |
182 | ```swift
183 | import OpenAI
184 |
185 | func openAI() async throws {
186 | let openAI = OpenAI(apiKey: "YOUR_API_KEY")
187 |
188 | let models = try await openAI.availableModels()
189 | let model = try await openAI.model(withId: "gpt-3.5-turbo")
190 | }
191 | ```
192 |
193 | ## Generating cURL Prompts
194 |
195 | Sometimes, it might be useful to see the generated requests, so all three classes introduced above come with methods that generate a usable `cURL` prompt from a request that you can simply paste into a terminal. For example:
196 |
197 | ```swift
198 | import ChatGPT
199 |
200 | func askChatGPT() async throws {
201 | let chatGPT = ChatGPT(apiKey: "YOUR_API_KEY", defaultModel: .gpt3)
202 |
203 | let request = ChatRequest.gpt3 { request in
204 | request.messages = [
205 | .init(role: .system, content: "You are the pilot of an alien UFO. Be creative."),
206 | .init(role: .user, content: "Where do you come from?")
207 | ]
208 | request.numberOfAnswers = 2
209 | }
210 |
211 | try await print(chatGPT.curl(for: request))
212 | }
213 | ```
214 |
215 | This generates the following:
216 |
217 | ```
218 | curl --request POST \
219 | --url 'https://api.openai.com/v1/chat/completions' \
220 | --header 'Accept: application/json' \
221 | --header 'Content-Type: application/json' \
222 | --header 'Authorization: Bearer YOUR_API_KEY' \
223 | --data '{"messages":[{"content":"You are the pilot of an alien UFO. Be creative.","role":"system"},{"content":"Where do you come from?","role":"user"}],"model":"gpt-3.5-turbo","n":2,"stream":false}' | json_pp
224 |
225 | ```
226 |
227 | ## License
228 |
229 | MIT License
230 |
231 | Copyright (c) 2023 Dennis Müller and all collaborators
232 |
233 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
234 |
235 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
236 |
237 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
238 |
--------------------------------------------------------------------------------
/Sources/Base/API.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright © 2023 Dennis Müller and all collaborators
3 | //
4 | // Permission is hereby granted, free of charge, to any person obtaining a copy
5 | // of this software and associated documentation files (the "Software"), to deal
6 | // in the Software without restriction, including without limitation the rights
7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | // copies of the Software, and to permit persons to whom the Software is
9 | // furnished to do so, subject to the following conditions:
10 | //
11 | // The above copyright notice and this permission notice shall be included in all
12 | // copies or substantial portions of the Software.
13 | //
14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20 | // SOFTWARE.
21 |
22 | import Foundation
23 |
24 | public struct API {
25 | private init() {}
26 | public static let base = "https://api.openai.com/"
27 | public static let v1ChatCompletion = "v1/chat/completions"
28 | public static let v1Completion = "v1/completions"
29 | public static let v1Models = "v1/models"
30 |
31 | public static func v1Model(withId id: String) -> String {
32 | "v1/models/\(id)"
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/Sources/Base/APIClientRequestHandler.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright © 2023 Dennis Müller and all collaborators
3 | //
4 | // Permission is hereby granted, free of charge, to any person obtaining a copy
5 | // of this software and associated documentation files (the "Software"), to deal
6 | // in the Software without restriction, including without limitation the rights
7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | // copies of the Software, and to permit persons to whom the Software is
9 | // furnished to do so, subject to the following conditions:
10 | //
11 | // The above copyright notice and this permission notice shall be included in all
12 | // copies or substantial portions of the Software.
13 | //
14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20 | // SOFTWARE.
21 |
22 | import Foundation
23 | import Get
24 | import GPTSwiftSharedTypes
25 |
26 | public func _addHeaders(to request: inout URLRequest, apiKey: String) {
27 | request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
28 | request.setValue("application/json", forHTTPHeaderField: "Content-Type")
29 | }
30 |
31 | public class _APIClientRequestHandler: APIClientDelegate {
32 | public let apiKey: String
33 |
34 | public init(apiKey: String) {
35 | self.apiKey = apiKey
36 | }
37 |
38 | public func client(_ client: APIClient, willSendRequest request: inout URLRequest) async throws {
39 | _addHeaders(to: &request, apiKey: apiKey)
40 | }
41 |
42 | public func client(_ client: APIClient, validateResponse response: HTTPURLResponse, data: Data, task: URLSessionTask) throws {
43 | if (200...299).contains(response.statusCode) { return }
44 |
45 | // If we can decode an error object from OpenAI, we use that as the error, as it contains useful information.
46 | if let openAIErrorDTO = try? JSONDecoder().decode(OpenAIErrorDTO.self, from: data) {
47 | throw openAIErrorDTO.error
48 | }
49 | }
50 | }
51 |
52 | /// Converts a `Swift.Error` object to a `GPTSwiftError`.
53 | /// - Parameter error: The error to convert.
54 | /// - Returns: A `GPTSwiftError`.
55 | public func _errorToGPTSwiftError(_ error: Swift.Error) -> GPTSwiftError {
56 | switch error {
57 | case let openAIError as OpenAIError:
58 | return .openAIError(openAIError)
59 | case let apiError as APIError:
60 | return .requestFailed(message: apiError.localizedDescription)
61 | case let urlError as URLError:
62 | return .urlError(urlError)
63 | default:
64 | return .other(error)
65 | }
66 | }
67 |
68 |
--------------------------------------------------------------------------------
/Sources/ChatGPT/ChatGPT+StreamedAnswer.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright © 2023 Dennis Müller and all collaborators
3 | //
4 | // Permission is hereby granted, free of charge, to any person obtaining a copy
5 | // of this software and associated documentation files (the "Software"), to deal
6 | // in the Software without restriction, including without limitation the rights
7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | // copies of the Software, and to permit persons to whom the Software is
9 | // furnished to do so, subject to the following conditions:
10 | //
11 | // The above copyright notice and this permission notice shall be included in all
12 | // copies or substantial portions of the Software.
13 | //
14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20 | // SOFTWARE.
21 | //
22 |
23 | import Foundation
24 | import Get
25 | import Base
26 | import GPTSwiftSharedTypes
27 |
28 | extension ChatGPT {
29 | public class StreamedAnswer {
30 | private let client: APIClient
31 | private let apiKey: String
32 | private let defaultModel: ChatGPTModel
33 |
34 | init(client: APIClient, apiKey: String, defaultModel: ChatGPTModel) {
35 | self.client = client
36 | self.apiKey = apiKey
37 | self.defaultModel = defaultModel
38 | }
39 |
40 | /// Ask ChatGPT a single prompt without any special configuration.
41 | /// - Parameter userPrompt: The prompt to send
42 | /// - Parameter systemPrompt: An optional system prompt to give GPT instructions on how to answer.
43 | /// - Parameter model: The model that should be used.
44 | /// - Returns: The response.
45 | /// - Throws: A `GPTSwiftError`.
46 | @available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *)
47 | public func ask(
48 | _ userPrompt: String,
49 | withSystemPrompt systemPrompt: String? = nil,
50 | model: ChatGPTModel = .default
51 | ) async throws -> AsyncThrowingStream {
52 | var messages: [ChatMessage] = []
53 |
54 | if let systemPrompt {
55 | messages.insert(.init(role: .system, content: systemPrompt), at: 0)
56 | }
57 |
58 | messages.append(.init(role: .user, content: userPrompt))
59 | let usingModel = model is DefaultChatGPTModel ? defaultModel : model
60 | let chatRequest = ChatRequest.streamed(
61 | model: usingModel,
62 | messages: messages
63 | )
64 |
65 | return try await ask(request: chatRequest)
66 | }
67 |
68 | /// Ask ChatGPT something by sending multiple messages without any special configuration.
69 | /// - Parameter messages: The chat messages.
70 | /// - Parameter model: The model that should be used.
71 | /// - Returns: The response.
72 | /// - Throws: A `GPTSwiftError`.
73 | @available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *)
74 | public func ask(
75 | messages: [ChatMessage],
76 | model: ChatGPTModel = .default
77 | ) async throws -> AsyncThrowingStream {
78 | let usingModel = model is DefaultChatGPTModel ? defaultModel : model
79 | let chatRequest = ChatRequest.streamed(model: usingModel, messages: messages)
80 | return try await ask(request: chatRequest)
81 | }
82 |
83 | /// Ask ChatGPT something by providing a chat request object, giving you full control over the request's configuration.
84 | /// - Parameter chatRequest: The request.
85 | /// - Returns: The response.
86 | /// - Throws: A `GPTSwiftError`.
87 | @available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *)
88 | public func ask(request chatRequest: ChatRequest) async throws -> AsyncThrowingStream {
89 |
90 | var chatRequest = chatRequest
91 | chatRequest.stream = true
92 |
93 | let request = Request(path: API.v1ChatCompletion, method: .post, body: chatRequest)
94 | var urlRequest = try await client.makeURLRequest(for: request)
95 | _addHeaders(to: &urlRequest, apiKey: apiKey)
96 |
97 | do {
98 | let (result, response) = try await client.session.bytes(for: urlRequest)
99 |
100 | guard let response = response as? HTTPURLResponse else {
101 | throw GPTSwiftError.responseParsingFailed
102 | }
103 |
104 | guard response.statusCode.isStatusCodeOkay else {
105 | throw GPTSwiftError.requestFailed(message: "Response status code was unacceptable: \(response.statusCode)")
106 | }
107 |
108 | return AsyncThrowingStream { continuation in
109 | Task {
110 | do {
111 | for try await line in result.lines {
112 | guard let chatResponse = line.asStreamedResponse else {
113 | break
114 | }
115 |
116 | // Ignore lines where only the role is specified
117 | if chatResponse.choices.first?.delta.role != nil {
118 | continue
119 | }
120 |
121 | guard let message = chatResponse.choices.first?.delta.content else {
122 | continue
123 | }
124 |
125 | // Yield next token
126 | continuation.yield(message)
127 | }
128 | } catch {
129 | throw GPTSwiftError.responseParsingFailed
130 | }
131 |
132 | continuation.finish()
133 | }
134 | }
135 | } catch {
136 | throw _errorToGPTSwiftError(error)
137 | }
138 | }
139 |
140 | /// Turns a chat request into a curl prompt that you can paste into a terminal.
141 | ///
142 | /// This method will change the provided chat request to include a `stream: true` argument.
143 | /// The rest of the request will not be changed.
144 | ///
145 | /// This might be useful for debugging to experimenting.
146 | /// Taken from [Abhishek Maurya](https://gist.github.com/abhi21git/3dc611aab9e1cf5e5343ba4b58573596) and slightly adjusted.
147 | /// - Parameters:
148 | /// - chatRequest: The request.
149 | /// - pretty: An option to make the curl prompt pretty.
150 | /// - Returns: The curl prompt.
151 | public func curl(for chatRequest: ChatRequest, pretty: Bool = true) async throws -> String {
152 | var chatRequest = chatRequest
153 | chatRequest.stream = true
154 | let request = Request(path: API.v1ChatCompletion, method: .post, body: chatRequest)
155 | var urlRequest = try await client.makeURLRequest(for: request)
156 | _addHeaders(to: &urlRequest, apiKey: apiKey)
157 | return urlRequest.curl(pretty: pretty, formatOutput: false)
158 | }
159 | }
160 | }
161 |
162 | private let decoder = JSONDecoder()
163 | private extension String {
164 | var asStreamedResponse: ChatStreamedResponse? {
165 | guard hasPrefix("data: "),
166 | let data = dropFirst(6).data(using: .utf8) else {
167 | return nil
168 | }
169 | return try? decoder.decode(ChatStreamedResponse.self, from: data)
170 | }
171 | }
172 |
173 | private extension Int {
174 | var isStatusCodeOkay: Bool {
175 | (200...299).contains(self)
176 | }
177 | }
178 |
--------------------------------------------------------------------------------
/Sources/ChatGPT/ChatGPT.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright © 2023 Dennis Müller and all collaborators
3 | //
4 | // Permission is hereby granted, free of charge, to any person obtaining a copy
5 | // of this software and associated documentation files (the "Software"), to deal
6 | // in the Software without restriction, including without limitation the rights
7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | // copies of the Software, and to permit persons to whom the Software is
9 | // furnished to do so, subject to the following conditions:
10 | //
11 | // The above copyright notice and this permission notice shall be included in all
12 | // copies or substantial portions of the Software.
13 | //
14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20 | // SOFTWARE.
21 | //
22 |
23 | import Foundation
24 | import Get
25 | import Base
26 | @_exported import GPTSwiftSharedTypes
27 |
28 | /// A simple and easy to use wrapper around the ChatGPT API from OpenAI, with support for GPT 3.5 turbo as well as GPT 4 and its large context variant.
29 | public class ChatGPT {
30 |
31 | private let client: APIClient
32 | private let apiKey: String
33 | private let apiClientRequestHandler: _APIClientRequestHandler
34 |
35 | private let defaultModel: ChatGPTModel
36 |
37 | /// A variant of `ChatGPT` that streams all the answers.
38 | ///
39 | /// This exposes the exact same methods as before, but it returns an `AsyncThrowingStream` that yields individual tokens as soon as they are generated by GPT.
40 | public let streamedAnswer: StreamedAnswer
41 |
42 | /// A simple and easy to use wrapper around the ChatGPT API from OpenAI, with support for GPT 3.5 turbo as well as GPT 4 and its large context variant.
43 | ///
44 | /// - Parameter apiKey: The api key you can generate in your account page on OpenAI's website.
45 | /// - Parameter defaultModel: Sets the default model for all requests coming from this `ChatGPT` instance.
46 | /// - Parameter urlSessionConfiguration: An optional URL session configuration object.
47 | public init(
48 | apiKey: String,
49 | defaultModel: ChatGPTModel = .gpt3,
50 | urlSessionConfiguration: URLSessionConfiguration? = nil
51 | ) {
52 | self.apiKey = apiKey
53 | self.apiClientRequestHandler = .init(apiKey: apiKey)
54 | self.defaultModel = defaultModel
55 | self.client = APIClient(baseURL: URL(string: API.base)) { [apiClientRequestHandler] configuration in
56 | configuration.delegate = apiClientRequestHandler
57 | if let urlSessionConfiguration {
58 | configuration.sessionConfiguration = urlSessionConfiguration
59 | }
60 | }
61 | self.streamedAnswer = .init(client: client, apiKey: apiKey, defaultModel: defaultModel)
62 | }
63 |
64 | /// Ask ChatGPT a single prompt without any special configuration.
65 | /// - Parameter userPrompt: The prompt to send
66 | /// - Parameter systemPrompt: an optional system prompt to give GPT instructions on how to answer.
67 | /// - Parameter model: The model that should be used.
68 | /// - Returns: The response as string.
69 | /// - Throws: A `GPTSwiftError` if the request fails or the server returns an unauthorized status code.
70 | public func ask(
71 | _ userPrompt: String,
72 | withSystemPrompt systemPrompt: String? = nil,
73 | model: ChatGPTModel = .default
74 | ) async throws -> String {
75 | var messages: [ChatMessage] = []
76 |
77 | if let systemPrompt {
78 | messages.insert(.init(role: .system, content: systemPrompt), at: 0)
79 | }
80 |
81 | messages.append(.init(role: .user, content: userPrompt))
82 |
83 | let usingModel = model is DefaultChatGPTModel ? defaultModel : model
84 | let request = Request(
85 | path: API.v1ChatCompletion,
86 | method: .post,
87 | body: ChatRequest(model: usingModel, messages: messages)
88 | )
89 |
90 | let response = try await send(request: request)
91 | guard let answer = response.choices.first?.message.content else {
92 | throw GPTSwiftError.responseParsingFailed
93 | }
94 |
95 | return answer
96 | }
97 |
98 | /// Ask ChatGPT something by sending multiple messages without any special configuration.
99 | /// - Parameter messages: The chat messages.
100 | /// - Parameter model: The model that should be used.
101 | /// - Returns: The response as string.
102 | /// - Throws: A `GPTSwiftError` if the request fails or the server returns an unauthorized status code.
103 | public func ask(
104 | messages: [ChatMessage],
105 | model: ChatGPTModel = .default
106 | ) async throws -> String {
107 | let usingModel = model is DefaultChatGPTModel ? defaultModel : model
108 | let request = Request(
109 | path: API.v1ChatCompletion,
110 | method: .post,
111 | body: ChatRequest(model: usingModel, messages: messages)
112 | )
113 |
114 | let response = try await send(request: request)
115 | guard let answer = response.choices.first?.message.content else {
116 | throw GPTSwiftError.responseParsingFailed
117 | }
118 |
119 | return answer
120 | }
121 |
122 | /// Ask ChatGPT something by providing a chat request object, giving you full control over the request's configuration.
123 | /// - Parameter request: The request.
124 | /// - Returns: The response.
125 | /// - Throws: A `GPTSwiftError` if the request fails or the server returns an unauthorized status code.
126 | public func ask(request: ChatRequest) async throws -> ChatResponse {
127 | let request = Request(
128 | path: API.v1ChatCompletion,
129 | method: .post,
130 | body: request
131 | )
132 |
133 | return try await send(request: request)
134 | }
135 |
136 | /// Turns a chat request into a curl prompt that you can paste into a terminal.
137 | ///
138 | /// This might be useful for debugging to experimenting.
139 | /// Taken from [Abhishek Maurya](https://gist.github.com/abhi21git/3dc611aab9e1cf5e5343ba4b58573596) and slightly adjusted.
140 | /// - Parameters:
141 | /// - chatRequest: The request.
142 | /// - pretty: An option to make the curl prompt pretty.
143 | /// - formatOutput: An option that, if set, puts the response through `json_pp` to prettify the json object.
144 | /// - Returns: The curl prompt.
145 | public func curl(for chatRequest: ChatRequest, pretty: Bool = true, formatOutput: Bool = true) async throws -> String {
146 | let request = Request(path: API.v1ChatCompletion, method: .post, body: chatRequest)
147 | var urlRequest = try await client.makeURLRequest(for: request)
148 | _addHeaders(to: &urlRequest, apiKey: apiKey)
149 | return urlRequest.curl(pretty: pretty, formatOutput: formatOutput)
150 | }
151 |
152 | /// Sends the request, catches all errors and replaces them with a `GPTSwiftError`. If successful, it returns the response value.
153 | /// - Parameter request: The request to send.
154 | /// - Returns: The response object, already decoded.
155 | private func send(request: Request) async throws -> Response {
156 | do {
157 | return try await client.send(request).value
158 | } catch {
159 | throw _errorToGPTSwiftError(error)
160 | }
161 | }
162 | }
163 |
--------------------------------------------------------------------------------
/Sources/ChatGPT/Models/ChatGPTModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright © 2023 Dennis Müller and all collaborators
3 | //
4 | // Permission is hereby granted, free of charge, to any person obtaining a copy
5 | // of this software and associated documentation files (the "Software"), to deal
6 | // in the Software without restriction, including without limitation the rights
7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | // copies of the Software, and to permit persons to whom the Software is
9 | // furnished to do so, subject to the following conditions:
10 | //
11 | // The above copyright notice and this permission notice shall be included in all
12 | // copies or substantial portions of the Software.
13 | //
14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20 | // SOFTWARE.
21 |
22 | import Foundation
23 |
24 | public protocol ChatGPTModel {
25 | var rawValue: String { get }
26 | }
27 |
28 | // MARK: - Default ChatGPTModel
29 |
30 | public struct DefaultChatGPTModel: ChatGPTModel {
31 | public var rawValue: String = ""
32 | }
33 |
34 | // MARK: - Custom ChatGPTModel
35 |
36 | public struct CustomChatGPTModel: ChatGPTModel {
37 | public var rawValue: String
38 | }
39 |
40 | // MARK: - GPT 3 ChatGPTModel
41 |
42 | public struct GPT3ChatGPTModel: ChatGPTModel {
43 | public var rawValue: String = "gpt-3.5-turbo"
44 |
45 | public func stableVersion(_ identifier: StableVersionIdentifier = .march_1_2023) -> GPT3ChatGPTModel {
46 | .init(rawValue: "gpt-3.5-turbo-0301")
47 | }
48 |
49 | public enum StableVersionIdentifier {
50 | case march_1_2023
51 | }
52 | }
53 |
54 | // MARK: - GPT 4 ChatGPTModel
55 |
56 | public struct GPT4ChatGPTModel: ChatGPTModel {
57 | public var rawValue: String = "gpt-4"
58 |
59 | public func stableVersion(_ identifier: StableVersionIdentifier = .march_14_2023) -> GPT4ChatGPTModel {
60 | .init(rawValue: "gpt-4-0314")
61 | }
62 |
63 | public enum StableVersionIdentifier {
64 | case march_14_2023
65 | }
66 | }
67 |
68 | // MARK: - GPT 4 ChatGPTModel With Large Context
69 |
70 | public struct GPT4LargeContextChatGPTModel: ChatGPTModel {
71 | public var rawValue: String = "gpt-4-32k"
72 |
73 | public func stableVersion(_ identifier: StableVersionIdentifier = .march_14_2023) -> GPT4ChatGPTModel {
74 | .init(rawValue: "gpt-4-32k-0314")
75 | }
76 |
77 | public enum StableVersionIdentifier {
78 | case march_14_2023
79 | }
80 | }
81 |
82 | // MARK: - Protocol Extensions
83 |
84 | public extension ChatGPTModel where Self == DefaultChatGPTModel {
85 | static var `default`: Self {
86 | DefaultChatGPTModel()
87 | }
88 | }
89 |
90 | public extension ChatGPTModel where Self == CustomChatGPTModel {
91 | /// A fallback model descriptor with which you can set a custom model id string yourself.
92 | ///
93 | /// Use this, if you need to use a model that is not explicitly provided by GPTSwift. Keep in mind, however, that the model has to behave exactly like the provided ones. Otherwise, the request will likely fail.
94 | /// - Parameter modelId: The model id to use, e.g. "gpt-4-some-awesome-variation".
95 | /// - Returns: The model descriptor.
96 | static func custom(modelId: String) -> Self {
97 | CustomChatGPTModel(rawValue: modelId)
98 | }
99 | }
100 |
101 | public extension ChatGPTModel where Self == GPT3ChatGPTModel {
102 | /// The GPT3 model descriptor.
103 | static var gpt3: GPT3ChatGPTModel {
104 | GPT3ChatGPTModel()
105 | }
106 | }
107 |
108 | public extension ChatGPTModel where Self == GPT4ChatGPTModel {
109 | /// The GPT4 model descriptor.
110 | static var gpt4: GPT4ChatGPTModel {
111 | GPT4ChatGPTModel()
112 | }
113 | }
114 |
115 | public extension ChatGPTModel where Self == GPT4LargeContextChatGPTModel {
116 | /// The GPT4 "Large Context" model descriptor.
117 | static var gpt4LargeContext: GPT4LargeContextChatGPTModel {
118 | GPT4LargeContextChatGPTModel()
119 | }
120 | }
121 |
--------------------------------------------------------------------------------
/Sources/ChatGPT/Models/ChatMessage.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright © 2023 Dennis Müller and all collaborators
3 | //
4 | // Permission is hereby granted, free of charge, to any person obtaining a copy
5 | // of this software and associated documentation files (the "Software"), to deal
6 | // in the Software without restriction, including without limitation the rights
7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | // copies of the Software, and to permit persons to whom the Software is
9 | // furnished to do so, subject to the following conditions:
10 | //
11 | // The above copyright notice and this permission notice shall be included in all
12 | // copies or substantial portions of the Software.
13 | //
14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20 | // SOFTWARE.
21 | //
22 |
23 | import Foundation
24 |
25 | /// A message is a part of the chat conversation.
26 | ///
27 | /// GPT does not remember any previous messages, so if you want to keep a conversation coherent,
28 | /// you should always send previous messages with every new chat request. It might also be useful to send GPT's answers with the request as well (using the message `role` `assistant`).
29 | ///
30 | /// For more about this, see the [OpenAI documentation](https://platform.openai.com/docs/guides/chat/introduction).
31 | public struct ChatMessage: Codable {
32 |
33 | /// The role of the message.
34 | public var role: ChatMessageRole
35 |
36 | /// The content of the message.
37 | public var content: String
38 |
39 | /// A message is a part of the chat conversation.
40 | public init(role: ChatMessageRole, content: String) {
41 | self.role = role
42 | self.content = content
43 | }
44 |
45 | /// Convenience method to create a message with the `user` role.
46 | /// - Parameter content: The content of the message.
47 | /// - Returns: An instance of `ChatMessage` with the `user` role.
48 | public static func user(_ content: String) -> Self {
49 | .init(role: .user, content: content)
50 | }
51 |
52 | /// Convenience method to create a message with the `system` role.
53 | /// - Parameter content: The content of the message.
54 | /// - Returns: An instance of `ChatMessage` with the `system` role.
55 | public static func system(_ content: String) -> Self {
56 | .init(role: .system, content: content)
57 | }
58 |
59 | /// Convenience method to create a message with the `assistant` role.
60 | /// - Parameter content: The content of the message.
61 | /// - Returns: An instance of `ChatMessage` with the `assistant` role.
62 | public static func assistant(_ content: String) -> Self {
63 | .init(role: .assistant, content: content)
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/Sources/ChatGPT/Models/ChatMessageRole.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by Dennis Müller on 05.03.23.
6 | //
7 |
8 | import Foundation
9 |
10 | /// A role can be seen as the "owner", or "author" of a given message. You use this to allow GPT to differentiate between actual user prompts, behavior instructions (by you, the developer) and previous answers by GPT.
11 | ///
12 | /// For more information, see the [OpenAI documentation](https://platform.openai.com/docs/guides/chat/introduction).
13 | public enum ChatMessageRole: String, Codable {
14 |
15 | /// The user role is the used for the prompts made by the end-user.
16 | case user
17 |
18 | /// The system role is used to instruct GPT on how to behave and what to generate.
19 | case system
20 |
21 | /// The assistant role is usually meant to indicate that a message originates from a previous GPT answer.
22 | ///
23 | /// This is useful because it allows GPT to recall previous answers and know about the general context of the conversation.
24 | case assistant
25 | }
26 |
--------------------------------------------------------------------------------
/Sources/ChatGPT/Models/ChatRequest.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright © 2023 Dennis Müller and all collaborators
3 | //
4 | // Permission is hereby granted, free of charge, to any person obtaining a copy
5 | // of this software and associated documentation files (the "Software"), to deal
6 | // in the Software without restriction, including without limitation the rights
7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | // copies of the Software, and to permit persons to whom the Software is
9 | // furnished to do so, subject to the following conditions:
10 | //
11 | // The above copyright notice and this permission notice shall be included in all
12 | // copies or substantial portions of the Software.
13 | //
14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20 | // SOFTWARE.
21 | //
22 |
23 | import Foundation
24 | import Base
25 |
26 | /// A chat request is the main interface to ChatGPT's API.
27 | ///
28 | /// For more information, see the [OpenAI documentation](https://platform.openai.com/docs/guides/chat/introduction).
29 | /// And for detailed information about the parameters, see the [API documentation](https://platform.openai.com/docs/api-reference/chat).
30 | public struct ChatRequest {
31 |
32 | /// The model to use.
33 | public var model: String
34 |
35 | /// The messages for the request.
36 | public var messages: [ChatMessage]
37 |
38 | /// The temperature for the request. This determines the randomness of the response.
39 | public var temperature: Double?
40 |
41 | /// An alternative way of controlling the temperature. Do not use at the same time as `temperature`.
42 | public var topP: Double?
43 |
44 | /// The number of answers that GPT should generate.
45 | ///
46 | /// The default is 1.
47 | public var numberOfAnswers: Double?
48 |
49 | /// A boolean flag indicating if the answers should be streamed.
50 | var stream: Bool
51 |
52 | /// Up to 4 sequences where the API will stop generating further tokens.
53 | public var stop: [String]?
54 |
55 | /// The maximum number of tokens allowed for the response. If this is `nil`, it will default to allow the maximum number of tokens (4096 - message tokens). This does not mean, that all answers will generate that many tokens.
56 | public var maximumTokens: Int?
57 |
58 | /// A mechanism to penalize the occurrence of new tokens based on whether they appear in the text so far.
59 | ///
60 | /// Should be a number between `-2.0` and `2.0`.
61 | public var presencePenalty: Double?
62 |
63 | /// A mechanism to penalize the occurrence of new tokens based on their frequency in the text.
64 | ///
65 | /// Should be a number between `-2.0` and `2.0`.
66 | public var frequencyPenalty: Double?
67 |
68 | /// Modifies the likelihood of specified tokens appearing in the answer.
69 | ///
70 | /// This maps token IDs to their bias value.
71 | public var logitBias: [String: Double]?
72 |
73 | /// An optional user identifier to help detect misuse of the API.
74 | public var user: String?
75 |
76 | /// A chat request is the main interface to ChatGPT's API.
77 | public init(
78 | model: ChatGPTModel = .gpt3,
79 | messages: [ChatMessage] = [],
80 | maximumTokens: Int? = nil,
81 | temperature: Double? = nil,
82 | topP: Double? = nil,
83 | numberOfAnswers: Double? = nil,
84 | stop: [String]? = nil,
85 | presencePenalty: Double? = nil,
86 | frequencyPenalty: Double? = nil,
87 | logitBias: [String : Double]? = nil,
88 | user: String? = nil
89 | ) {
90 | self.model = model.rawValue
91 | self.messages = messages
92 | self.maximumTokens = maximumTokens
93 | self.temperature = temperature
94 | self.topP = topP
95 | self.numberOfAnswers = numberOfAnswers
96 | self.stop = stop
97 | self.presencePenalty = presencePenalty
98 | self.frequencyPenalty = frequencyPenalty
99 | self.logitBias = logitBias
100 | self.user = user
101 | self.stream = false
102 | }
103 |
104 | /// Convenience initializer that sets the model to `gpt3` and configures the request with the provided closure.
105 | /// - Parameters:
106 | /// - configure: the configuration.
107 | /// - Returns: A configured instance of `ChatRequest`.
108 | public static func gpt3(configuration configure: ((_ request: inout ChatRequest) -> Void)? = nil) -> ChatRequest {
109 | var request = ChatRequest(model: .gpt3)
110 | configure?(&request)
111 | return request
112 | }
113 |
114 | /// Convenience initializer that sets the model to `gpt4` and configures the request with the provided closure.
115 | /// - Parameters:
116 | /// - configure: the configuration.
117 | /// - Returns: A configured instance of `ChatRequest`.
118 | public static func gpt4(configuration configure: ((_ request: inout ChatRequest) -> Void)? = nil) -> ChatRequest {
119 | var request = ChatRequest(model: .gpt4)
120 | configure?(&request)
121 | return request
122 | }
123 |
124 | /// Convenience initializer that sets the model to `gpt4LargeContext` and configures the request with the provided closure.
125 | /// - Parameters:
126 | /// - configure: the configuration.
127 | /// - Returns: A configured instance of `ChatRequest`.
128 | public static func gpt4LargeContext(configuration configure: ((_ request: inout ChatRequest) -> Void)? = nil) -> ChatRequest {
129 | var request = ChatRequest(model: .gpt4LargeContext)
130 | configure?(&request)
131 | return request
132 | }
133 |
134 | static func streamed(model: ChatGPTModel, messages: [ChatMessage]) -> Self {
135 | var request = ChatRequest(model: model, messages: messages)
136 | request.stream = true
137 | return request
138 | }
139 | }
140 |
141 | extension ChatRequest: Codable {
142 |
143 | enum CodingKeys: String, CodingKey {
144 | case model
145 | case messages
146 | case stop
147 | case maximumTokens = "max_tokens"
148 | case temperature
149 | case topP = "top_p"
150 | case numberOfAnswers = "n"
151 | case presencePenalty = "presence_penalty"
152 | case frequencyPenalty = "frequency_penalty"
153 | case logitBias = "logit_bias"
154 | case stream
155 | case user
156 | }
157 | }
158 |
--------------------------------------------------------------------------------
/Sources/ChatGPT/Models/ChatResponse.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright © 2023 Dennis Müller and all collaborators
3 | //
4 | // Permission is hereby granted, free of charge, to any person obtaining a copy
5 | // of this software and associated documentation files (the "Software"), to deal
6 | // in the Software without restriction, including without limitation the rights
7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | // copies of the Software, and to permit persons to whom the Software is
9 | // furnished to do so, subject to the following conditions:
10 | //
11 | // The above copyright notice and this permission notice shall be included in all
12 | // copies or substantial portions of the Software.
13 | //
14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20 | // SOFTWARE.
21 |
22 | /// The response for a GPT `ChatRequest`.
23 | public struct ChatResponse: Codable {
24 |
25 | /// An identifier for the response.
26 | public var id: String
27 |
28 | /// The description of the response. Usually, this should be "chat.completion".
29 | public var object: String
30 |
31 | /// The creation date of the response.
32 | public var created: Int
33 |
34 | /// The model used for generating the response.
35 | public var model: String
36 |
37 | /// Usage statistics, for example the used tokens.
38 | public var usage: Usage
39 |
40 | /// The answers that GPT generated.
41 | public var choices: [Choice]
42 | }
43 |
44 | // MARK: - Subtypes
45 |
46 | extension ChatResponse {
47 |
48 | /// Usage statistics, for example the used tokens.
49 | public struct Usage: Codable {
50 |
51 | /// The number of tokens used to encode the request messages.
52 | public var promptTokens: Int
53 |
54 | /// The number of tokens used to encode the answers.
55 | public var completionTokens: Int
56 |
57 | /// The total number of tokens used.
58 | public var totalTokens: Int
59 |
60 | public enum CodingKeys: String, CodingKey {
61 | case promptTokens = "prompt_tokens"
62 | case completionTokens = "completion_tokens"
63 | case totalTokens = "total_tokens"
64 | }
65 | }
66 | }
67 |
68 | extension ChatResponse {
69 |
70 | /// An answer that GPT generated.
71 | public struct Choice: Codable {
72 |
73 | /// The actual message of the answer.
74 | public var message: ChatMessage
75 |
76 | /// An optional reason for the termination of the message.
77 | ///
78 | /// For example, this can be "stop" to indicate GPT considers the answer as done.
79 | public var finishReason: String?
80 |
81 | /// The index of the message in the messages array of `CompletionResponse`.
82 | public var index: Int
83 |
84 | public enum CodingKeys: String, CodingKey {
85 | case message
86 | case finishReason = "finish_reason"
87 | case index
88 | }
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/Sources/ChatGPT/Models/ChatStreamedMessage.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright © 2023 Dennis Müller and all collaborators
3 | //
4 | // Permission is hereby granted, free of charge, to any person obtaining a copy
5 | // of this software and associated documentation files (the "Software"), to deal
6 | // in the Software without restriction, including without limitation the rights
7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | // copies of the Software, and to permit persons to whom the Software is
9 | // furnished to do so, subject to the following conditions:
10 | //
11 | // The above copyright notice and this permission notice shall be included in all
12 | // copies or substantial portions of the Software.
13 | //
14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20 | // SOFTWARE.
21 | //
22 |
23 | import Foundation
24 |
25 | /// A streamed message is a part of the chat conversation.
26 | ///
27 | /// For more about this, see the [OpenAI documentation](https://platform.openai.com/docs/guides/chat/introduction).
28 | struct ChatStreamedMessage: Codable {
29 |
30 | /// The role of the message.
31 | var role: ChatMessageRole?
32 |
33 | /// The content of the message.
34 | var content: String?
35 |
36 | /// A message is a part of the chat conversation.
37 | init(role: ChatMessageRole?, content: String?) {
38 | self.role = role
39 | self.content = content
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/Sources/ChatGPT/Models/ChatStreamedResponse.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright © 2023 Dennis Müller and all collaborators
3 | //
4 | // Permission is hereby granted, free of charge, to any person obtaining a copy
5 | // of this software and associated documentation files (the "Software"), to deal
6 | // in the Software without restriction, including without limitation the rights
7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | // copies of the Software, and to permit persons to whom the Software is
9 | // furnished to do so, subject to the following conditions:
10 | //
11 | // The above copyright notice and this permission notice shall be included in all
12 | // copies or substantial portions of the Software.
13 | //
14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20 | // SOFTWARE.
21 |
22 | /// The response for a GPT `ChatRequest` that is streamed.
23 | struct ChatStreamedResponse: Codable {
24 |
25 | /// An identifier for the response.
26 | var id: String
27 |
28 | /// The description of the response. Usually, this should be "chat.completion".
29 | var object: String
30 |
31 | /// The creation date of the response.
32 | var created: Int
33 |
34 | /// The model used for generating the response.
35 | var model: String
36 |
37 | /// The answers that GPT generated.
38 | var choices: [Choice]
39 | }
40 |
41 | extension ChatStreamedResponse {
42 |
43 | /// An answer that GPT generated.
44 | struct Choice: Codable {
45 |
46 | /// The actual message of the answer.
47 | var delta: ChatStreamedMessage
48 |
49 | /// An optional reason for the termination of the message.
50 | ///
51 | /// For example, this can be "stop" to indicate GPT considers the answer as done.
52 | var finishReason: String?
53 |
54 | /// The index of the message in the messages array of `CompletionResponse`.
55 | var index: Int
56 |
57 | enum CodingKeys: String, CodingKey {
58 | case delta
59 | case finishReason = "finish_reason"
60 | case index
61 | }
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/Sources/GPT/GPT+StreamedAnswer.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright © 2023 Dennis Müller and all collaborators
3 | //
4 | // Permission is hereby granted, free of charge, to any person obtaining a copy
5 | // of this software and associated documentation files (the "Software"), to deal
6 | // in the Software without restriction, including without limitation the rights
7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | // copies of the Software, and to permit persons to whom the Software is
9 | // furnished to do so, subject to the following conditions:
10 | //
11 | // The above copyright notice and this permission notice shall be included in all
12 | // copies or substantial portions of the Software.
13 | //
14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20 | // SOFTWARE.
21 | //
22 |
23 | import Foundation
24 | import Get
25 | import Base
26 | import GPTSwiftSharedTypes
27 |
28 | extension GPT {
29 | public class StreamedAnswer {
30 | private let client: APIClient
31 | private let apiKey: String
32 | private let defaultModel: GPTModel
33 |
34 | init(client: APIClient, apiKey: String, defaultModel: GPTModel) {
35 | self.client = client
36 | self.apiKey = apiKey
37 | self.defaultModel = defaultModel
38 | }
39 |
40 | /// Generates a completion for the given user prompt using the specified model or the default model, streaming the response token by token.
41 | ///
42 | /// This method is a convenience method for the "Create completion" OpenAI API endpoint with the `stream` option enabled.
43 | ///
44 | /// - Parameter userPrompt: The prompt to complete.
45 | /// - Parameter model: The GPT model to use for generating the completion. Defaults to `.default`.
46 | /// - Returns: An `AsyncThrowingStream` of `String` objects representing tokens in the generated completion.
47 | /// - Throws: A `Swift.Error` if the request fails or the server returns an unauthorized status code.
48 | @available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *)
49 | public func complete(
50 | _ userPrompt: String,
51 | model: GPTModel = .default
52 | ) async throws -> AsyncThrowingStream {
53 | let usingModel = model is DefaultGPTModel ? defaultModel : model
54 | let completionRequest = CompletionRequest.streamed(
55 | model: usingModel,
56 | prompt: userPrompt
57 | )
58 |
59 | return try await complete(request: completionRequest)
60 | }
61 |
62 | /// Generates a completion for the given `CompletionRequest`, streaming the response token by token.
63 | ///
64 | /// This method provides full control over the completion request and corresponds to the "Create completion" OpenAI API endpoint with the `stream` option enabled.
65 | ///
66 | /// - Parameter completionRequest: A `CompletionRequest` object containing the details of the completion request.
67 | ///
68 | /// - Returns: An `AsyncThrowingStream` of `String` objects representing tokens in the generated completion.
69 | /// - Throws: A `Swift.Error` if the request fails or the server returns an unauthorized status code.
70 | @available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *)
71 | public func complete(request completionRequest: CompletionRequest) async throws -> AsyncThrowingStream {
72 |
73 | var completionRequest = completionRequest
74 | completionRequest.stream = true
75 |
76 | let request = Request(path: API.v1Completion, method: .post, body: completionRequest)
77 | var urlRequest = try await client.makeURLRequest(for: request)
78 | _addHeaders(to: &urlRequest, apiKey: apiKey)
79 |
80 | do {
81 | let (result, response) = try await client.session.bytes(for: urlRequest)
82 |
83 | guard let response = response as? HTTPURLResponse else {
84 | throw GPTSwiftError.responseParsingFailed
85 | }
86 |
87 | guard response.statusCode.isStatusCodeOkay else {
88 | throw GPTSwiftError.requestFailed(message: "Response status code was unacceptable: \(response.statusCode)")
89 | }
90 |
91 | return AsyncThrowingStream { continuation in
92 | Task {
93 | do {
94 | for try await line in result.lines {
95 | guard let chatResponse = line.asStreamedResponse else {
96 | break
97 | }
98 |
99 | guard let message = chatResponse.choices.first?.text else {
100 | continue
101 | }
102 |
103 | // Yield next token
104 | continuation.yield(message)
105 | }
106 | } catch {
107 | throw GPTSwiftError.responseParsingFailed
108 | }
109 |
110 | continuation.finish()
111 | }
112 | }
113 | } catch {
114 | throw _errorToGPTSwiftError(error)
115 | }
116 | }
117 |
118 | /// Turns a completion request into a curl prompt that you can paste into a terminal.
119 | ///
120 | /// This method will change the provided completion request to include a `stream: true` argument.
121 | /// The rest of the request will not be changed.
122 | ///
123 | /// This might be useful for debugging to experimenting.
124 | /// Taken from [Abhishek Maurya](https://gist.github.com/abhi21git/3dc611aab9e1cf5e5343ba4b58573596) and slightly adjusted.
125 | /// - Parameters:
126 | /// - completionRequest: The request.
127 | /// - pretty: An option to make the curl prompt pretty.
128 | /// - Returns: The curl prompt.
129 | public func curl(for completionRequest: CompletionRequest, pretty: Bool = true) async throws -> String {
130 | var completionRequest = completionRequest
131 | completionRequest.stream = true
132 | let request = Request(path: API.v1Completion, method: .post, body: completionRequest)
133 | var urlRequest = try await client.makeURLRequest(for: request)
134 | _addHeaders(to: &urlRequest, apiKey: apiKey)
135 | return urlRequest.curl(pretty: pretty, formatOutput: false)
136 | }
137 | }
138 | }
139 |
140 | private let decoder = JSONDecoder()
141 | private extension String {
142 | var asStreamedResponse: CompletionStreamedResponse? {
143 | guard hasPrefix("data: "),
144 | let data = dropFirst(6).data(using: .utf8) else {
145 | return nil
146 | }
147 | return try? decoder.decode(CompletionStreamedResponse.self, from: data)
148 | }
149 | }
150 |
151 | private extension Int {
152 | var isStatusCodeOkay: Bool {
153 | (200...299).contains(self)
154 | }
155 | }
156 |
--------------------------------------------------------------------------------
/Sources/GPT/GPT.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright © 2023 Dennis Müller and all collaborators
3 | //
4 | // Permission is hereby granted, free of charge, to any person obtaining a copy
5 | // of this software and associated documentation files (the "Software"), to deal
6 | // in the Software without restriction, including without limitation the rights
7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | // copies of the Software, and to permit persons to whom the Software is
9 | // furnished to do so, subject to the following conditions:
10 | //
11 | // The above copyright notice and this permission notice shall be included in all
12 | // copies or substantial portions of the Software.
13 | //
14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20 | // SOFTWARE.
21 | //
22 |
23 | import Foundation
24 | import Get
25 | import Base
26 | @_exported import GPTSwiftSharedTypes
27 |
28 | /// A simple wrapper around the API for OpenAI's GPT.
29 | public class GPT {
30 |
31 | private let client: APIClient
32 | private let apiKey: String
33 | private let apiClientRequestHandler: _APIClientRequestHandler
34 | private let defaultModel: GPTModel
35 |
36 | /// A variant of `GPT` that streams all the answers.
37 | ///
38 | /// This exposes the exact same methods as before, but it returns an `AsyncThrowingStream` that yields individual tokens as soon as they are generated by GPT.
39 | public let streamedAnswer: StreamedAnswer
40 |
41 | /// A simple wrapper around the API for OpenAI.
42 | /// - Parameter apiKey: The api key you can generate in your account page on OpenAI's website.
43 | /// - Parameter defaultModel: Sets the default model for all requests coming from this `GPT` instance.
44 | /// - Parameter urlSessionConfiguration: An optional URL session configuration object.
45 | public init(
46 | apiKey: String,
47 | defaultModel: GPTModel = .davinci,
48 | urlSessionConfiguration: URLSessionConfiguration? = nil
49 | ) {
50 | self.apiKey = apiKey
51 | self.apiClientRequestHandler = .init(apiKey: apiKey)
52 | self.defaultModel = defaultModel
53 | self.client = APIClient(baseURL: URL(string: API.base)) { [apiClientRequestHandler] configuration in
54 | configuration.delegate = apiClientRequestHandler
55 | if let urlSessionConfiguration {
56 | configuration.sessionConfiguration = urlSessionConfiguration
57 | }
58 | }
59 | self.streamedAnswer = .init(client: client, apiKey: apiKey, defaultModel: defaultModel)
60 | }
61 |
62 | /// Generates a completion for the given user prompt using the specified model or the default model.
63 | ///
64 | /// This method is a convenience method for the "Create completion" OpenAI API endpoint.
65 | ///
66 | /// - Parameter userPrompt: The prompt to complete.
67 | /// - Parameter model: The GPT model to use for generating the completion. Defaults to `.default`.
68 | ///
69 | /// - Returns: A `CompletionResponse` object containing the generated completion.
70 | /// - Throws: A `GPTSwiftError` if the request fails or the server returns an unauthorized status code.
71 | public func complete(
72 | _ userPrompt: String,
73 | model: GPTModel = .default
74 | ) async throws -> CompletionResponse {
75 | let usingModel = model is DefaultGPTModel ? defaultModel : model
76 | let request = Request(
77 | path: API.v1Completion,
78 | method: .post,
79 | body: CompletionRequest(model: usingModel, prompt: userPrompt)
80 | )
81 |
82 | return try await send(request: request)
83 | }
84 |
85 | /// Generates a completion for the given `CompletionRequest`.
86 | ///
87 | /// This method provides full control over the completion request and corresponds to the "Create completion" OpenAI API endpoint.
88 | ///
89 | /// - Parameter completionRequest: A `CompletionRequest` object containing the details of the completion request.
90 | ///
91 | /// - Returns: A `CompletionResponse` object containing the generated completion.
92 | /// - Throws: A `GPTSwiftError` if the request fails or the server returns an unauthorized status code.
93 | public func complete(request completionRequest: CompletionRequest) async throws -> CompletionResponse {
94 | let request = Request(
95 | path: API.v1Completion,
96 | method: .post,
97 | body: completionRequest
98 | )
99 |
100 | return try await send(request: request)
101 | }
102 |
103 | /// Turns a completion request into a curl prompt that you can paste into a terminal.
104 | ///
105 | /// This might be useful for debugging to experimenting.
106 | /// Taken from [Abhishek Maurya](https://gist.github.com/abhi21git/3dc611aab9e1cf5e5343ba4b58573596) and slightly adjusted.
107 | /// - Parameters:
108 | /// - completionRequest: The request.
109 | /// - pretty: An option to make the curl prompt pretty.
110 | /// - formatOutput: An option that, if set, puts the response through `json_pp` to prettify the json object.
111 | /// - Returns: The curl prompt.
112 | public func curl(for completionRequest: CompletionRequest, pretty: Bool = true, formatOutput: Bool = true) async throws -> String {
113 | let request = Request(path: API.v1Completion, method: .post, body: completionRequest)
114 | var urlRequest = try await client.makeURLRequest(for: request)
115 | _addHeaders(to: &urlRequest, apiKey: apiKey)
116 | return urlRequest.curl(pretty: pretty, formatOutput: formatOutput)
117 | }
118 |
119 | /// Sends the request, catches all errors and replaces them with a `GPTSwiftError`. If successful, it returns the response value.
120 | /// - Parameter request: The request to send.
121 | /// - Returns: The response object, already decoded.
122 | private func send(request: Request) async throws -> Response {
123 | do {
124 | return try await client.send(request).value
125 | } catch {
126 | throw _errorToGPTSwiftError(error)
127 | }
128 | }
129 | }
130 |
--------------------------------------------------------------------------------
/Sources/GPT/Models/CompletionRequest.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright © 2023 Dennis Müller and all collaborators
3 | //
4 | // Permission is hereby granted, free of charge, to any person obtaining a copy
5 | // of this software and associated documentation files (the "Software"), to deal
6 | // in the Software without restriction, including without limitation the rights
7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | // copies of the Software, and to permit persons to whom the Software is
9 | // furnished to do so, subject to the following conditions:
10 | //
11 | // The above copyright notice and this permission notice shall be included in all
12 | // copies or substantial portions of the Software.
13 | //
14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20 | // SOFTWARE.
21 | //
22 |
23 | import Foundation
24 | import Base
25 |
26 | public struct CompletionRequest {
27 |
28 | /// The model to use.
29 | ///
30 | /// The default is to use the current model, that continuously receives updates.
31 | public var model: String
32 |
33 | /// The prompt for the request.
34 | public var prompt: String
35 |
36 | /// The suffix that comes after a completion.
37 | public var suffix: String?
38 |
39 | /// The maximum number of tokens allowed for the response. If this is `nil`, it will default to allow the maximum number of tokens (4096 - message tokens). This does not mean, that all answers will generate that many tokens.
40 | public var maximumTokens: Int?
41 |
42 | /// The temperature for the request. This determines the randomness of the response.
43 | public var temperature: Double?
44 |
45 | /// An alternative way of controlling the temperature. Do not use at the same time as `temperature`.
46 | public var topP: Double?
47 |
48 | /// The number of answers that GPT should generate.
49 | ///
50 | /// The default is 1.
51 | public var numberOfAnswers: Double?
52 |
53 | public var logProbabilities: Int?
54 |
55 | public var includePromptsInResponse: Bool
56 |
57 | public var stopSequences: [String]?
58 |
59 | /// A mechanism to penalize the occurrence of new tokens based on whether they appear in the text so far.
60 | ///
61 | /// Should be a number between `-2.0` and `2.0`.
62 | public var presencePenalty: Double?
63 |
64 | /// A mechanism to penalize the occurrence of new tokens based on their frequency in the text.
65 | ///
66 | /// Should be a number between `-2.0` and `2.0`.
67 | public var frequencyPenalty: Double?
68 |
69 | /// Generates n results server-side, where n is the provided value, and returns the one with the highest log probability per token.
70 | public var bestOf: Int?
71 |
72 | /// Modifies the likelihood of specified tokens appearing in the answer.
73 | ///
74 | /// This maps token IDs to their bias value.
75 | public var logitBias: [String: Double]?
76 |
77 | /// An optional user identifier to help detect misuse of the API.
78 | public var user: String?
79 |
80 | var stream: Bool
81 |
82 | /// A chat request is the main interface to ChatGPT's API.
83 | public init(
84 | model: GPTModel = .davinci,
85 | prompt: String,
86 | suffix: String? = nil,
87 | maximumTokens: Int? = nil,
88 | temperature: Double? = nil,
89 | topP: Double? = nil,
90 | numberOfAnswers: Double? = nil,
91 | logProbabilities: Int? = nil,
92 | includePromptsInResponse: Bool = false,
93 | stopSequences: [String]? = nil,
94 | presencePenalty: Double? = nil,
95 | frequencyPenalty: Double? = nil,
96 | bestOf: Int? = nil,
97 | logitBias: [String : Double]? = nil,
98 | user: String? = nil
99 | ) {
100 | self.model = model.rawValue
101 | self.prompt = prompt
102 | self.suffix = suffix
103 | self.maximumTokens = maximumTokens
104 | self.temperature = temperature
105 | self.topP = topP
106 | self.numberOfAnswers = numberOfAnswers
107 | self.logProbabilities = logProbabilities
108 | self.includePromptsInResponse = includePromptsInResponse
109 | self.stopSequences = stopSequences
110 | self.presencePenalty = presencePenalty
111 | self.frequencyPenalty = frequencyPenalty
112 | self.bestOf = bestOf
113 | self.logitBias = logitBias
114 | self.user = user
115 | self.stream = false
116 | }
117 |
118 | /// Convenience initializer that sets the model to `davinci` and configures the request with the provided closure.
119 | /// - Parameters:
120 | /// - prompt: The prompt for the request.
121 | /// - configure: the configuration.
122 | /// - Returns: A configured instance of `CompletionRequest`.
123 | public static func davinci(prompt: String, configuration configure: ((_ request: inout CompletionRequest) -> Void)? = nil) -> CompletionRequest {
124 | var request = CompletionRequest(model: .davinci, prompt: prompt)
125 | configure?(&request)
126 | return request
127 | }
128 |
129 | static func streamed(model: GPTModel, prompt: String) -> Self {
130 | var request = CompletionRequest(model: model, prompt: prompt)
131 | request.stream = true
132 | return request
133 | }
134 | }
135 |
136 | extension CompletionRequest: Codable {
137 | enum CodingKeys: String, CodingKey {
138 | case model
139 | case prompt
140 | case suffix
141 | case maximumTokens = "max_tokens"
142 | case temperature
143 | case topP = "top_p"
144 | case logProbabilities = "logprobs"
145 | case includePromptsInResponse = "echo"
146 | case stopSequences = "stop"
147 | case numberOfAnswers = "n"
148 | case presencePenalty = "presence_penalty"
149 | case frequencyPenalty = "frequency_penalty"
150 | case bestOf = "best_of"
151 | case logitBias = "logit_bias"
152 | case stream
153 | case user
154 | }
155 | }
156 |
--------------------------------------------------------------------------------
/Sources/GPT/Models/CompletionResponse.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright © 2023 Dennis Müller and all collaborators
3 | //
4 | // Permission is hereby granted, free of charge, to any person obtaining a copy
5 | // of this software and associated documentation files (the "Software"), to deal
6 | // in the Software without restriction, including without limitation the rights
7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | // copies of the Software, and to permit persons to whom the Software is
9 | // furnished to do so, subject to the following conditions:
10 | //
11 | // The above copyright notice and this permission notice shall be included in all
12 | // copies or substantial portions of the Software.
13 | //
14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20 | // SOFTWARE.
21 |
22 | /// The response for a GPT `ChatRequest`.
23 | public struct CompletionResponse: Codable {
24 |
25 | /// An identifier for the response.
26 | public var id: String
27 |
28 | /// The description of the response. Usually, this should be "chat.completion".
29 | public var object: String
30 |
31 | /// The creation date of the response.
32 | public var created: Int
33 |
34 | /// The model used for generating the response.
35 | public var model: String
36 |
37 | /// Usage statistics, for example the used tokens.
38 | public var usage: Usage
39 |
40 | /// The answers that GPT generated.
41 | public var choices: [Choice]
42 | }
43 |
44 | // MARK: - Subtypes
45 |
46 | extension CompletionResponse {
47 |
48 | /// Usage statistics, for example the used tokens.
49 | public struct Usage: Codable {
50 |
51 | /// The number of tokens used to encode the request messages.
52 | public var promptTokens: Int
53 |
54 | /// The number of tokens used to encode the answers.
55 | public var completionTokens: Int
56 |
57 | /// The total number of tokens used.
58 | public var totalTokens: Int
59 |
60 | public enum CodingKeys: String, CodingKey {
61 | case promptTokens = "prompt_tokens"
62 | case completionTokens = "completion_tokens"
63 | case totalTokens = "total_tokens"
64 | }
65 | }
66 | }
67 |
68 | extension CompletionResponse {
69 |
70 | /// An answer that GPT generated.
71 | public struct Choice: Codable {
72 |
73 | /// The actual message of the answer.
74 | public var text: String
75 |
76 | /// An optional reason for the termination of the message.
77 | ///
78 | /// For example, this can be "stop" to indicate GPT considers the answer as done.
79 | public var finishReason: String?
80 |
81 | /// The index of the message in the messages array of `CompletionResponse`.
82 | public var index: Int
83 |
84 | public enum CodingKeys: String, CodingKey {
85 | case text
86 | case finishReason = "finish_reason"
87 | case index
88 | }
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/Sources/GPT/Models/CompletionStreamedResponse.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright © 2023 Dennis Müller and all collaborators
3 | //
4 | // Permission is hereby granted, free of charge, to any person obtaining a copy
5 | // of this software and associated documentation files (the "Software"), to deal
6 | // in the Software without restriction, including without limitation the rights
7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | // copies of the Software, and to permit persons to whom the Software is
9 | // furnished to do so, subject to the following conditions:
10 | //
11 | // The above copyright notice and this permission notice shall be included in all
12 | // copies or substantial portions of the Software.
13 | //
14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20 | // SOFTWARE.
21 |
22 | /// The response for a GPT `CompletionResponse` that is streamed.
23 | struct CompletionStreamedResponse: Codable {
24 | var id: String
25 | var object: String
26 | var created: Int
27 | var model: String
28 | var choices: [Choice]
29 | }
30 |
31 | extension CompletionStreamedResponse {
32 | struct Choice: Codable {
33 | var text: String
34 | var finishReason: String?
35 | var logProbabilities: Double?
36 | var index: Int
37 |
38 | enum CodingKeys: String, CodingKey {
39 | case text
40 | case finishReason = "finish_reason"
41 | case logProbabilities = "logProbs"
42 | case index
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/Sources/GPT/Models/GPTModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright © 2023 Dennis Müller and all collaborators
3 | //
4 | // Permission is hereby granted, free of charge, to any person obtaining a copy
5 | // of this software and associated documentation files (the "Software"), to deal
6 | // in the Software without restriction, including without limitation the rights
7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | // copies of the Software, and to permit persons to whom the Software is
9 | // furnished to do so, subject to the following conditions:
10 | //
11 | // The above copyright notice and this permission notice shall be included in all
12 | // copies or substantial portions of the Software.
13 | //
14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20 | // SOFTWARE.
21 |
22 | import Foundation
23 |
24 | public protocol GPTModel {
25 | var rawValue: String { get }
26 | }
27 |
28 | // MARK: - Default ChatGPTModel
29 |
30 | public struct DefaultGPTModel: GPTModel {
31 | public var rawValue: String = ""
32 | }
33 |
34 | // MARK: - Custom GPTModel
35 |
36 | public struct CustomGPTModel: GPTModel {
37 | public var rawValue: String
38 | }
39 |
40 | // MARK: - GPT 3 Model
41 |
42 | public struct DavinciGPTModel: GPTModel {
43 | public var rawValue: String = "text-davinci-003"
44 | }
45 |
46 | // MARK: - Protocol Extensions
47 |
48 | public extension GPTModel where Self == DefaultGPTModel {
49 | static var `default`: Self {
50 | DefaultGPTModel()
51 | }
52 | }
53 |
54 | public extension GPTModel where Self == CustomGPTModel {
55 | /// A fallback model descriptor with which you can set a custom model id string yourself.
56 | ///
57 | /// Use this, if you need to use a model that is not explicitly provided by GPTSwift. Keep in mind, however, that the model has to behave exactly like the provided ones. Otherwise, the request will likely fail.
58 | /// - Parameter modelId: The model id to use, e.g. "gpt-4-some-awesome-variation".
59 | /// - Returns: The model descriptor.
60 | static func custom(modelId: String) -> Self {
61 | CustomGPTModel(rawValue: modelId)
62 | }
63 | }
64 |
65 | public extension GPTModel where Self == DavinciGPTModel {
66 | /// The davinci model descriptor.
67 | static var davinci: DavinciGPTModel {
68 | DavinciGPTModel()
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/Sources/GPTSwiftSharedTypes/GPTSwiftError.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright © 2023 Dennis Müller and all collaborators
3 | //
4 | // Permission is hereby granted, free of charge, to any person obtaining a copy
5 | // of this software and associated documentation files (the "Software"), to deal
6 | // in the Software without restriction, including without limitation the rights
7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | // copies of the Software, and to permit persons to whom the Software is
9 | // furnished to do so, subject to the following conditions:
10 | //
11 | // The above copyright notice and this permission notice shall be included in all
12 | // copies or substantial portions of the Software.
13 | //
14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20 | // SOFTWARE.
21 |
22 | import Foundation
23 |
24 | /// A custom error type representing errors related to GPTSwift.
25 | public struct GPTSwiftError: Swift.Error {
26 |
27 | /// Creates a new `GPTSwiftError` instance with the request failed error kind.
28 | ///
29 | /// - Parameter message: A string describing the error that occurred during the request.
30 | /// - Returns: A new `GPTSwiftError` instance.
31 | public static func requestFailed(message: String) -> Self {
32 | .init(kind: .requestFailed(message))
33 | }
34 |
35 | /// Creates a new `GPTSwiftError` instance with the response parsing failed error kind.
36 | ///
37 | /// - Returns: A new `GPTSwiftError` instance.
38 | public static var responseParsingFailed: Self {
39 | .init(kind: .responseParsingFailed)
40 | }
41 |
42 | /// Creates a new `GPTSwiftError` instance with the URL error kind.
43 | ///
44 | /// - Parameter error: The `URLError` instance that occurred.
45 | /// - Returns: A new `GPTSwiftError` instance.
46 | public static func urlError(_ error: URLError) -> Self {
47 | .init(kind: .urlError(error))
48 | }
49 |
50 | /// Creates a new `GPTSwiftError` instance with the other error kind.
51 | ///
52 | /// - Parameter error: The `Error` instance representing the unknown error that occurred.
53 | /// - Returns: A new `GPTSwiftError` instance.
54 | public static func other(_ error: Swift.Error) -> Self {
55 | .init(kind: .other(error))
56 | }
57 |
58 | /// Creates a new `GPTSwiftError` instance with the OpenAI error kind.
59 | ///
60 | /// - Parameter error: The `OpenAIError` instance that occurred.
61 | /// - Returns: A new `GPTSwiftError` instance.
62 | public static func openAIError(_ error: OpenAIError) -> Self {
63 | .init(kind: .openAIError(error))
64 | }
65 |
66 | /// The specific kind of `GPTSwiftError`.
67 | public var kind: Kind
68 |
69 | /// A string prefix used for error messages.
70 | private var prefix: String { "GPTSwiftError: " }
71 |
72 | /// A string describing the error.
73 | public var message: String {
74 | switch kind {
75 | case .requestFailed(let message):
76 | return prefix + message
77 | case .responseParsingFailed:
78 | return prefix + "Something went wrong with parsing the response."
79 | case .urlError(let error):
80 | return prefix + error.localizedDescription
81 | case .other(let error):
82 | return prefix + "An unknown error occurred: »\(error)«"
83 | case .openAIError(let error):
84 | return prefix + "(OpenAI response) »\(error.message)« - type »\(error.type)« - code: \(error.code ?? "not provided")"
85 | }
86 | }
87 |
88 | /// Initializes a new `GPTSwiftError` instance with the given kind.
89 | ///
90 | /// - Parameter kind: The specific kind of `GPTSwiftError`.
91 | init(kind: Kind) {
92 | self.kind = kind
93 | }
94 |
95 | /// A localized description of the error.
96 | public var localizedDescription: String {
97 | message
98 | }
99 | }
100 |
101 | /// An extension to `GPTSwiftError` containing an enumeration for different error kinds.
102 | extension GPTSwiftError {
103 | public enum Kind {
104 | /// An error kind indicating that a request failed.
105 | ///
106 | /// - Associated value: A string describing the error that occurred during the request.
107 | case requestFailed(String)
108 |
109 | /// An error kind indicating that parsing the response failed.
110 | case responseParsingFailed
111 |
112 | /// An error kind indicating that a URL error occurred.
113 | ///
114 | /// - Associated value: The `URLError` instance that occurred.
115 | case urlError(URLError)
116 |
117 | /// An error kind indicating that an OpenAI error occurred.
118 | ///
119 | /// - Associated value: The `OpenAIError` instance that occurred.
120 | case openAIError(OpenAIError)
121 |
122 | /// An error kind indicating that an unknown error occurred.
123 | ///
124 | /// - Associated value: The `Error` instance representing the unknown error that occurred.
125 | case other(Swift.Error)
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/Sources/GPTSwiftSharedTypes/OpenAIError.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright © 2023 Dennis Müller and all collaborators
3 | //
4 | // Permission is hereby granted, free of charge, to any person obtaining a copy
5 | // of this software and associated documentation files (the "Software"), to deal
6 | // in the Software without restriction, including without limitation the rights
7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | // copies of the Software, and to permit persons to whom the Software is
9 | // furnished to do so, subject to the following conditions:
10 | //
11 | // The above copyright notice and this permission notice shall be included in all
12 | // copies or substantial portions of the Software.
13 | //
14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20 | // SOFTWARE.
21 |
22 | import Foundation
23 |
24 | public struct OpenAIErrorDTO: Codable {
25 | public var error: OpenAIError
26 | }
27 |
28 | /// An error object from the OpenAI API.
29 | public struct OpenAIError: Swift.Error, Codable {
30 |
31 | /// A description of the error.
32 | public var message: String
33 |
34 | /// A type identifier for the error.
35 | public var type: String
36 |
37 | /// A code identifying the error.
38 | public var code: String?
39 | }
40 |
--------------------------------------------------------------------------------
/Sources/GPTSwiftSharedTypes/URLRequest+curl.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright © 2023 Dennis Müller and all collaborators
3 | //
4 | // Permission is hereby granted, free of charge, to any person obtaining a copy
5 | // of this software and associated documentation files (the "Software"), to deal
6 | // in the Software without restriction, including without limitation the rights
7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | // copies of the Software, and to permit persons to whom the Software is
9 | // furnished to do so, subject to the following conditions:
10 | //
11 | // The above copyright notice and this permission notice shall be included in all
12 | // copies or substantial portions of the Software.
13 | //
14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20 | // SOFTWARE.
21 |
22 | import Foundation
23 |
24 | extension URLRequest {
25 |
26 | /// Turns a request into a curl prompt that you can paste into a terminal.
27 | ///
28 | /// This might be useful for debugging to experimenting.
29 | ///
30 | /// Taken from https://gist.github.com/abhi21git/3dc611aab9e1cf5e5343ba4b58573596 and slightly adjusted.
31 | /// - Parameters:
32 | /// - pretty: An option to make the curl prompt pretty.
33 | /// - formatOutput: An option that, if set, puts the response through `json_pp` to prettify the json object.
34 | /// - Returns: The curl prompt
35 | public func curl(pretty: Bool = true, formatOutput: Bool = true) -> String {
36 | let newLine = pretty ? "\\\n" : ""
37 | let method = (pretty ? "--request " : "-X ") + "\(httpMethod ?? "GET") \(newLine)"
38 | let url: String = (pretty ? "--url " : "") + "\'\(url?.absoluteString ?? "")\' \(newLine)"
39 |
40 | var curl = "curl "
41 | var header = ""
42 | var data: String = ""
43 |
44 | if let httpHeaders = allHTTPHeaderFields, httpHeaders.keys.count > 0 {
45 | for (key,value) in httpHeaders {
46 | header += (pretty ? "--header " : "-H ") + "\'\(key): \(value)\' \(newLine)"
47 | }
48 | }
49 |
50 | if let bodyData = httpBody, let bodyString = String(data: bodyData, encoding: .utf8), !bodyString.isEmpty {
51 | data = "--data '\(bodyString)'"
52 | }
53 |
54 | curl += method + url + header + data
55 |
56 | if formatOutput {
57 | curl += " | json_pp"
58 | }
59 |
60 | return curl
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/Sources/OpenAI/Models/ModelList.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright © 2023 Dennis Müller and all collaborators
3 | //
4 | // Permission is hereby granted, free of charge, to any person obtaining a copy
5 | // of this software and associated documentation files (the "Software"), to deal
6 | // in the Software without restriction, including without limitation the rights
7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | // copies of the Software, and to permit persons to whom the Software is
9 | // furnished to do so, subject to the following conditions:
10 | //
11 | // The above copyright notice and this permission notice shall be included in all
12 | // copies or substantial portions of the Software.
13 | //
14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20 | // SOFTWARE.
21 | //
22 |
23 | import Foundation
24 | import Get
25 | import Base
26 |
27 | struct ModelListResponse: Codable {
28 | var data: [ModelData]
29 | }
30 |
31 | public struct ModelData: Codable {
32 | var id: String
33 | var object: String
34 | var owner: String
35 | var root: String
36 | var permission: [Permissions]
37 |
38 | public enum CodingKeys: String, CodingKey {
39 | case id
40 | case object
41 | case owner = "owned_by"
42 | case root
43 | case permission
44 | }
45 |
46 | public struct Permissions: Codable {
47 | public var id: String
48 | public var object: String
49 | public var created: Int
50 | public var allowCreateEngine: Bool
51 | public var allowSampling: Bool
52 | public var allowLogprobs: Bool
53 | public var allowSearchIndices: Bool
54 | public var allowView: Bool
55 | public var allowFineTuning: Bool
56 | public var organization: String
57 | public var group: String?
58 | public var isBlocking: Bool
59 |
60 | public enum CodingKeys: String, CodingKey {
61 | case id
62 | case object
63 | case created
64 | case allowCreateEngine = "allow_create_engine"
65 | case allowSampling = "allow_sampling"
66 | case allowLogprobs = "allow_logprobs"
67 | case allowSearchIndices = "allow_search_indices"
68 | case allowView = "allow_view"
69 | case allowFineTuning = "allow_fine_tuning"
70 | case organization
71 | case group
72 | case isBlocking = "is_blocking"
73 | }
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/Sources/OpenAI/OpenAI.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright © 2023 Dennis Müller and all collaborators
3 | //
4 | // Permission is hereby granted, free of charge, to any person obtaining a copy
5 | // of this software and associated documentation files (the "Software"), to deal
6 | // in the Software without restriction, including without limitation the rights
7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | // copies of the Software, and to permit persons to whom the Software is
9 | // furnished to do so, subject to the following conditions:
10 | //
11 | // The above copyright notice and this permission notice shall be included in all
12 | // copies or substantial portions of the Software.
13 | //
14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20 | // SOFTWARE.
21 | //
22 |
23 | import Foundation
24 | import Get
25 | import Base
26 | @_exported import GPTSwiftSharedTypes
27 |
28 | /// A class for interacting with the OpenAI API to fetch information about available models and retrieve specific models.
29 | public class OpenAI {
30 |
31 | /// The API client used for making API requests.
32 | private let client: APIClient
33 |
34 | /// The api key.
35 | private let apiKey: String
36 |
37 | /// The API client request handler used for managing API request configurations.
38 | private let apiClientRequestHandler: _APIClientRequestHandler
39 |
40 | /// Initializes a new instance of the OpenAI class with the provided API key.
41 | ///
42 | /// - Parameter apiKey: The API key used for authentication when making API requests.
43 | /// - Parameter urlSessionConfiguration: An optional URL session configuration object.
44 | public init(
45 | apiKey: String,
46 | urlSessionConfiguration: URLSessionConfiguration? = nil
47 | ) {
48 | self.apiKey = apiKey
49 | self.apiClientRequestHandler = .init(apiKey: apiKey)
50 | self.client = APIClient(baseURL: URL(string: API.base)) { [apiClientRequestHandler] configuration in
51 | configuration.delegate = apiClientRequestHandler
52 | if let urlSessionConfiguration {
53 | configuration.sessionConfiguration = urlSessionConfiguration
54 | }
55 | }
56 | }
57 |
58 | /// Asynchronously fetches a list of available models from the OpenAI API.
59 | ///
60 | /// This method corresponds to the "List models" API endpoint.
61 | ///
62 | /// - Returns: An array of `ModelData` objects representing the available models.
63 | /// - Throws: A `GPTSwiftError` if the request fails or the server returns an unauthorized status code.
64 | public func availableModels() async throws -> [ModelData] {
65 | let request = Request(path: API.v1Models)
66 | return try await send(request: request).data
67 | }
68 |
69 | /// Asynchronously retrieves a specific model from the OpenAI API using the provided model ID.
70 | ///
71 | /// This method corresponds to the "Retrieve model" API endpoint.
72 | ///
73 | /// - Parameter id: The ID of the model to retrieve.
74 | /// - Returns: A `ModelData` object representing the requested model.
75 | /// - Throws: A `GPTSwiftError` if the request fails or the server returns an unauthorized status code.
76 | public func model(withId id: String) async throws -> ModelData {
77 | let request = Request(path: API.v1Model(withId: id))
78 | return try await send(request: request)
79 | }
80 |
81 | /// Returns a usable curl prompt that you can paste into a terminal to receive all available models.
82 | ///
83 | /// This might be useful for debugging to experimenting.
84 | /// Taken from [Abhishek Maurya](https://gist.github.com/abhi21git/3dc611aab9e1cf5e5343ba4b58573596) and slightly adjusted.
85 | /// - Parameters:
86 | /// - pretty: An option to make the curl prompt pretty.
87 | /// - formatOutput: An option that, if set, puts the response through `json_pp` to prettify the json object.
88 | /// - Returns: The curl prompt.
89 | public func curlForAvailableModels(pretty: Bool = true, formatOutput: Bool = true) async throws -> String {
90 | let request = Request(path: API.v1Models)
91 | var urlRequest = try await client.makeURLRequest(for: request)
92 | _addHeaders(to: &urlRequest, apiKey: apiKey)
93 | return urlRequest.curl(pretty: pretty, formatOutput: formatOutput)
94 | }
95 |
96 | /// Returns a usable curl prompt that you can paste into a terminal to receive the model with the given id.
97 | ///
98 | /// This might be useful for debugging to experimenting.
99 | /// Taken from [Abhishek Maurya](https://gist.github.com/abhi21git/3dc611aab9e1cf5e5343ba4b58573596) and slightly adjusted.
100 | /// - Parameters:
101 | /// - id: The model id.
102 | /// - pretty: An option to make the curl prompt pretty.
103 | /// - formatOutput: An option that, if set, puts the response through `json_pp` to prettify the json object.
104 | /// - Returns: The curl prompt.
105 | public func curlForModel(withId id: String, pretty: Bool = true, formatOutput: Bool = true) async throws -> String {
106 | let request = Request(path: API.v1Model(withId: id))
107 | var urlRequest = try await client.makeURLRequest(for: request)
108 | _addHeaders(to: &urlRequest, apiKey: apiKey)
109 | return urlRequest.curl(pretty: pretty, formatOutput: formatOutput)
110 | }
111 |
112 | /// Sends the request, catches all errors and replaces them with a `GPTSwiftError`. If successful, it returns the response value.
113 | /// - Parameter request: The request to send.
114 | /// - Returns: The response object, already decoded.
115 | private func send(request: Request) async throws -> Response {
116 | do {
117 | return try await client.send(request).value
118 | } catch {
119 | throw _errorToGPTSwiftError(error)
120 | }
121 | }
122 | }
123 |
--------------------------------------------------------------------------------