Skip to content

Commit 868319d

Browse files
committed
Update to use Official ChatGPT API with turbo model
1 parent 8490530 commit 868319d

File tree

7 files changed

+139
-81
lines changed

7 files changed

+139
-81
lines changed

README.MD

Lines changed: 2 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -2,50 +2,17 @@
22

33
![Alt text](https://imagizer.imageshack.com/v2/640x480q90/922/hmlopw.png "image")
44

5-
This is a native iOS, macOS, watchOS, tvOS App for interacting with ChatGPT built using SwiftUI and OpenAPI API. It used the leaked model and special prompt to access ChatGPT using Open AI Completions API endpoint.
6-
7-
## DISCLAIMERS!
8-
The `ChatGPTAPI.swift` is updated frequently when new model is found. Expect breaking changes. Always read readme before updating or opening an issue.
9-
10-
Use this at your own risk, there is a possibility that OpenAI might ban your account using this approach! I don't take any responsibility.
5+
This is a native iOS, macOS, watchOS, tvOS App for interacting with ChatGPT built using SwiftUI and OpenAPI API. It's using Official ChatGPT endpoint with `gpt-3.5-turbo` model.
116

127
## Separate SPM repo for API only
138
you can add dependency for [ChatGPTSwift](https://github.com/alfianlosari/ChatGPTSwift) to access the API only if you want to integrate into your own app
149

15-
## UPDATE 6 - 9 Feb 2023
16-
17-
Add separate target for tvOS App
18-
19-
![Alt text](https://imagizer.imageshack.com/v2/640x480q90/924/3cnQLj.png "image")
20-
21-
## UPDATE 5 - 8 Feb 2023
22-
The leaked model had been removed by OpenAI. Until a new model is found, i'll use the default `text-davinci-003`
23-
24-
## UPDATE 4 - 7 Feb 2023
25-
26-
Add separate target for watchOS independent App
27-
28-
![Alt text](https://imagizer.imageshack.com/v2/640x480q90/923/Hk89yV.png "image")
29-
30-
## UPDATE 4 - 5 Feb 2023
31-
Add separate target for macOS Menu Bar App
32-
33-
![Alt text](https://imagizer.imageshack.com/v2/640x480q90/923/CufOj0.png "image")
34-
35-
## UPDATE 3 - 4 Feb 2023
36-
Update to a much shorter prompt for triggering ChatGPT
37-
38-
## UPDATE 2 - 3 Feb 2023
39-
It's working again after updating to the latest *model*
40-
41-
## UPDATE 1
42-
At the time this video is published, the leaked model to access ChatGPT using Completion API endpoint had been taken down by OpenAI, so it won't work. But most of the concept should remain the same for building the application UI and state management. When the official API is released, you can simply update to use the public model and official endpoint for ChatGPT :) I'll also update the GitHub repo and create a follow-up video when it will be released in near future.
43-
4410
## Video tutorial
4511
- [iOS YouTube](https://youtu.be/PLEgTCT20zU)
4612
- [macOS YouTube](https://youtu.be/Wl1cDvwpJoE)
4713
- [watchOS YouTube](https://youtu.be/DwXy0gKz1GY)
4814
- [tvOS YouTube](https://youtu.be/7RQHG7GXJ_U)
15+
- [Upgrade to Official API YouTube](https://youtu.be/9byLhs5hQjI)
4916

5017
## Requierements
5118
- Xcode 14

Shared/ChatGPTAPI.swift

Lines changed: 52 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,17 @@
77

88
import Foundation
99

10-
class ChatGPTAPI {
10+
class ChatGPTAPI: @unchecked Sendable {
11+
12+
private let systemMessage: Message
13+
private let temperature: Double
14+
private let model: String
1115

1216
private let apiKey: String
13-
private var historyList = [String]()
17+
private var historyList = [Message]()
1418
private let urlSession = URLSession.shared
1519
private var urlRequest: URLRequest {
16-
let url = URL(string: "https://api.openai.com/v1/completions")!
20+
let url = URL(string: "https://api.openai.com/v1/chat/completions")!
1721
var urlRequest = URLRequest(url: url)
1822
urlRequest.httpMethod = "POST"
1923
headers.forEach { urlRequest.setValue($1, forHTTPHeaderField: $0) }
@@ -26,13 +30,11 @@ class ChatGPTAPI {
2630
return df
2731
}()
2832

29-
private let jsonDecoder = JSONDecoder()
30-
private var basePrompt: String {
31-
"You are ChatGPT, a large language model trained by OpenAI. Respond conversationally. Do not answer as the user. Current date: \(dateFormatter.string(from: Date()))"
32-
+ "\n\n"
33-
+ "User: Hello\n"
34-
+ "ChatGPT: Hello! How can I help you today? <|im_end|>\n\n\n"
35-
}
33+
private let jsonDecoder: JSONDecoder = {
34+
let jsonDecoder = JSONDecoder()
35+
jsonDecoder.keyDecodingStrategy = .convertFromSnakeCase
36+
return jsonDecoder
37+
}()
3638

3739
private var headers: [String: String] {
3840
[
@@ -41,40 +43,33 @@ class ChatGPTAPI {
4143
]
4244
}
4345

44-
private var historyListText: String {
45-
historyList.joined()
46-
}
47-
48-
init(apiKey: String) {
46+
47+
init(apiKey: String, model: String = "gpt-3.5-turbo", systemPrompt: String = "You are a helpful assistant", temperature: Double = 0.5) {
4948
self.apiKey = apiKey
49+
self.model = model
50+
self.systemMessage = .init(role: "system", content: systemPrompt)
51+
self.temperature = temperature
5052
}
5153

52-
private func generateChatGPTPrompt(from text: String) -> String {
53-
var prompt = basePrompt + historyListText + "User: \(text)\nChatGPT:"
54-
if prompt.count > (4000 * 4) {
54+
private func generateMessages(from text: String) -> [Message] {
55+
var messages = [systemMessage] + historyList + [Message(role: "user", content: text)]
56+
57+
if messages.contentCount > (4000 * 4) {
5558
_ = historyList.dropFirst()
56-
prompt = generateChatGPTPrompt(from: text)
59+
messages = generateMessages(from: text)
5760
}
58-
return prompt
61+
return messages
5962
}
6063

6164
private func jsonBody(text: String, stream: Bool = true) throws -> Data {
62-
let jsonBody: [String: Any] = [
63-
"model": "text-davinci-003",
64-
"temperature": 0.5,
65-
"max_tokens": 1024,
66-
"prompt": generateChatGPTPrompt(from: text),
67-
"stop": [
68-
"\n\n\n",
69-
"<|im_end|>"
70-
],
71-
"stream": stream
72-
]
73-
return try JSONSerialization.data(withJSONObject: jsonBody)
65+
let request = Request(model: model, temperature: temperature,
66+
messages: generateMessages(from: text), stream: stream)
67+
return try JSONEncoder().encode(request)
7468
}
7569

7670
private func appendToHistoryList(userText: String, responseText: String) {
77-
self.historyList.append("User: \(userText)\n\n\nChatGPT: \(responseText)<|im_end|>\n")
71+
self.historyList.append(.init(role: "user", content: userText))
72+
self.historyList.append(.init(role: "assistant", content: responseText))
7873
}
7974

8075
func sendMessageStream(text: String) async throws -> AsyncThrowingStream<String, Error> {
@@ -88,18 +83,28 @@ class ChatGPTAPI {
8883
}
8984

9085
guard 200...299 ~= httpResponse.statusCode else {
91-
throw "Bad Response: \(httpResponse.statusCode)"
86+
var errorText = ""
87+
for try await line in result.lines {
88+
errorText += line
89+
}
90+
91+
if let data = errorText.data(using: .utf8), let errorResponse = try? jsonDecoder.decode(ErrorRootResponse.self, from: data).error {
92+
errorText = "\n\(errorResponse.message)"
93+
}
94+
95+
throw "Bad Response: \(httpResponse.statusCode), \(errorText)"
9296
}
9397

9498
return AsyncThrowingStream<String, Error> { continuation in
95-
Task(priority: .userInitiated) {
99+
Task(priority: .userInitiated) { [weak self] in
100+
guard let self else { return }
96101
do {
97102
var responseText = ""
98103
for try await line in result.lines {
99104
if line.hasPrefix("data: "),
100105
let data = line.dropFirst(6).data(using: .utf8),
101-
let response = try? self.jsonDecoder.decode(CompletionResponse.self, from: data),
102-
let text = response.choices.first?.text {
106+
let response = try? self.jsonDecoder.decode(StreamCompletionResponse.self, from: data),
107+
let text = response.choices.first?.delta.content {
103108
responseText += text
104109
continuation.yield(text)
105110
}
@@ -124,18 +129,26 @@ class ChatGPTAPI {
124129
}
125130

126131
guard 200...299 ~= httpResponse.statusCode else {
127-
throw "Bad Response: \(httpResponse.statusCode)"
132+
var error = "Bad Response: \(httpResponse.statusCode)"
133+
if let errorResponse = try? jsonDecoder.decode(ErrorRootResponse.self, from: data).error {
134+
error.append("\n\(errorResponse.message)")
135+
}
136+
throw error
128137
}
129138

130139
do {
131140
let completionResponse = try self.jsonDecoder.decode(CompletionResponse.self, from: data)
132-
let responseText = completionResponse.choices.first?.text ?? ""
141+
let responseText = completionResponse.choices.first?.message.content ?? ""
133142
self.appendToHistoryList(userText: text, responseText: responseText)
134143
return responseText
135144
} catch {
136145
throw error
137146
}
138147
}
148+
149+
func deleteHistoryList() {
150+
self.historyList.removeAll()
151+
}
139152
}
140153

141154
extension String: CustomNSError {
@@ -147,10 +160,4 @@ extension String: CustomNSError {
147160
}
148161
}
149162

150-
struct CompletionResponse: Decodable {
151-
let choices: [Choice]
152-
}
153163

154-
struct Choice: Decodable {
155-
let text: String
156-
}

Shared/ChatGPTAPIModels.swift

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
//
2+
// ChatGPTAPIModels.swift
3+
// XCAChatGPT
4+
//
5+
// Created by Alfian Losari on 03/03/23.
6+
//
7+
8+
import Foundation
9+
10+
struct Message: Codable {
11+
let role: String
12+
let content: String
13+
}
14+
15+
extension Array where Element == Message {
16+
17+
var contentCount: Int { reduce(0, { $0 + $1.content.count })}
18+
}
19+
20+
struct Request: Codable {
21+
let model: String
22+
let temperature: Double
23+
let messages: [Message]
24+
let stream: Bool
25+
}
26+
27+
struct ErrorRootResponse: Decodable {
28+
let error: ErrorResponse
29+
}
30+
31+
struct ErrorResponse: Decodable {
32+
let message: String
33+
let type: String?
34+
}
35+
36+
struct StreamCompletionResponse: Decodable {
37+
let choices: [StreamChoice]
38+
}
39+
40+
struct CompletionResponse: Decodable {
41+
let choices: [Choice]
42+
let usage: Usage?
43+
}
44+
45+
struct Usage: Decodable {
46+
let promptTokens: Int?
47+
let completionTokens: Int?
48+
let totalTokens: Int?
49+
}
50+
51+
struct Choice: Decodable {
52+
let message: Message
53+
let finishReason: String?
54+
}
55+
56+
struct StreamChoice: Decodable {
57+
let finishReason: String?
58+
let delta: StreamMessage
59+
}
60+
61+
struct StreamMessage: Decodable {
62+
let role: String?
63+
let content: String?
64+
}
65+

Shared/MessageRow.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ struct MessageRow: Identifiable {
1717
let sendText: String
1818

1919
let responseImage: String
20-
var responseText: String
20+
var responseText: String?
2121

2222
var responseError: String?
2323

Shared/ViewModel.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ class ViewModel: ObservableObject {
3636
@MainActor
3737
func clearMessages() {
3838
stopSpeaking()
39+
api.deleteHistoryList()
3940
withAnimation { [weak self] in
4041
self?.messages = []
4142
}

XCAChatGPT.xcodeproj/project.pbxproj

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@
3636
8B057623298FBE0400A56C9A /* DotLoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B91C017298ADF4E0079AF26 /* DotLoadingView.swift */; };
3737
8B057624298FBE0400A56C9A /* ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B91C013298ADC560079AF26 /* ViewModel.swift */; };
3838
8B05764829909A9200A56C9A /* ScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B05764729909A9200A56C9A /* ScrollView.swift */; };
39+
8B82463429B1F49F0069B8F7 /* ChatGPTAPIModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B82463329B1F49F0069B8F7 /* ChatGPTAPIModels.swift */; };
40+
8B82463529B1F49F0069B8F7 /* ChatGPTAPIModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B82463329B1F49F0069B8F7 /* ChatGPTAPIModels.swift */; };
41+
8B82463629B1F49F0069B8F7 /* ChatGPTAPIModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B82463329B1F49F0069B8F7 /* ChatGPTAPIModels.swift */; };
42+
8B82463729B1F49F0069B8F7 /* ChatGPTAPIModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B82463329B1F49F0069B8F7 /* ChatGPTAPIModels.swift */; };
3943
8B91C004298AD09E0079AF26 /* XCAChatGPTApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B91C003298AD09E0079AF26 /* XCAChatGPTApp.swift */; };
4044
8B91C006298AD09E0079AF26 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B91C005298AD09E0079AF26 /* ContentView.swift */; };
4145
8B91C008298AD09F0079AF26 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8B91C007298AD09F0079AF26 /* Assets.xcassets */; };
@@ -87,6 +91,7 @@
8791
8B057617298FBDB700A56C9A /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
8892
8B05761A298FBDB700A56C9A /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
8993
8B05764729909A9200A56C9A /* ScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollView.swift; sourceTree = "<group>"; };
94+
8B82463329B1F49F0069B8F7 /* ChatGPTAPIModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatGPTAPIModels.swift; sourceTree = "<group>"; };
9095
8B91C000298AD09E0079AF26 /* XCAChatGPT.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = XCAChatGPT.app; sourceTree = BUILT_PRODUCTS_DIR; };
9196
8B91C003298AD09E0079AF26 /* XCAChatGPTApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCAChatGPTApp.swift; sourceTree = "<group>"; };
9297
8B91C005298AD09E0079AF26 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
@@ -154,6 +159,7 @@
154159
isa = PBXGroup;
155160
children = (
156161
8B91C011298AD0CE0079AF26 /* ChatGPTAPI.swift */,
162+
8B82463329B1F49F0069B8F7 /* ChatGPTAPIModels.swift */,
157163
8B91C005298AD09E0079AF26 /* ContentView.swift */,
158164
8B91C017298ADF4E0079AF26 /* DotLoadingView.swift */,
159165
8B91C019298ADF7F0079AF26 /* MessageRowView.swift */,
@@ -432,6 +438,7 @@
432438
files = (
433439
8B057597298E52DD00A56C9A /* MessageRow.swift in Sources */,
434440
8B057593298E52D900A56C9A /* ChatGPTAPI.swift in Sources */,
441+
8B82463529B1F49F0069B8F7 /* ChatGPTAPIModels.swift in Sources */,
435442
8B057594298E52D900A56C9A /* ContentView.swift in Sources */,
436443
8B057592298E52D900A56C9A /* DotLoadingView.swift in Sources */,
437444
8B057595298E52D900A56C9A /* MessageRowView.swift in Sources */,
@@ -446,6 +453,7 @@
446453
files = (
447454
8B057608298FA9E900A56C9A /* DotLoadingView.swift in Sources */,
448455
8B057606298FA9E900A56C9A /* MessageRowView.swift in Sources */,
456+
8B82463629B1F49F0069B8F7 /* ChatGPTAPIModels.swift in Sources */,
449457
8B057609298FA9E900A56C9A /* MessageRow.swift in Sources */,
450458
8B057605298FA9E900A56C9A /* ContentView.swift in Sources */,
451459
8B05760A298FA9E900A56C9A /* ChatGPTAPI.swift in Sources */,
@@ -460,6 +468,7 @@
460468
files = (
461469
8B057621298FBE0400A56C9A /* MessageRow.swift in Sources */,
462470
8B057622298FBE0400A56C9A /* MessageRowView.swift in Sources */,
471+
8B82463729B1F49F0069B8F7 /* ChatGPTAPIModels.swift in Sources */,
463472
8B05761F298FBE0400A56C9A /* ChatGPTAPI.swift in Sources */,
464473
8B057623298FBE0400A56C9A /* DotLoadingView.swift in Sources */,
465474
8B057614298FBDB600A56C9A /* XCAChatGPTTVApp.swift in Sources */,
@@ -475,6 +484,7 @@
475484
files = (
476485
8B91C012298AD0CE0079AF26 /* ChatGPTAPI.swift in Sources */,
477486
8B91C006298AD09E0079AF26 /* ContentView.swift in Sources */,
487+
8B82463429B1F49F0069B8F7 /* ChatGPTAPIModels.swift in Sources */,
478488
8B91C014298ADC560079AF26 /* ViewModel.swift in Sources */,
479489
8B91C018298ADF4E0079AF26 /* DotLoadingView.swift in Sources */,
480490
8B91C004298AD09E0079AF26 /* XCAChatGPTApp.swift in Sources */,

XCAChatGPT/XCAChatGPTApp.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,14 @@ struct XCAChatGPTApp: App {
1616
WindowGroup {
1717
NavigationStack {
1818
ContentView(vm: vm)
19+
.toolbar {
20+
ToolbarItem {
21+
Button("Clear") {
22+
vm.clearMessages()
23+
}
24+
.disabled(vm.isInteractingWithChatGPT)
25+
}
26+
}
1927
}
2028
}
2129
}

0 commit comments

Comments
 (0)