Skip to content

Commit c81430c

Browse files
feat: adds Predicted Outputs (#17)
1 parent 11a19e7 commit c81430c

File tree

7 files changed

+375
-0
lines changed

7 files changed

+375
-0
lines changed

Playground/Playground/Views/AppView.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ struct AppView: View {
3030
}
3131

3232
if provider == .openai {
33+
NavigationLink("Predicted Outputs") {
34+
PredictedOutputsView(provider: provider)
35+
}
36+
3337
NavigationLink("Response Format") {
3438
ResponseFormatView(provider: provider)
3539
}
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
//
2+
// PredictedOutputsView.swift
3+
// Playground
4+
//
5+
// Created by Kevin Hermawan on 11/5/24.
6+
//
7+
8+
import SwiftUI
9+
import LLMChatOpenAI
10+
11+
struct PredictedOutputsView: View {
12+
let provider: ServiceProvider
13+
14+
@Environment(AppViewModel.self) private var viewModel
15+
@State private var isPreferencesPresented: Bool = false
16+
17+
@State private var prompt: String = "Replace the Username property with an Email property. Respond only with code, and with no markdown formatting."
18+
@State private var response: String = ""
19+
@State private var acceptedPredictionTokens: Int = 0
20+
@State private var rejectedPredictionTokens: Int = 0
21+
@State private var inputTokens: Int = 0
22+
@State private var outputTokens: Int = 0
23+
@State private var totalTokens: Int = 0
24+
25+
private let prediction = """
26+
/// <summary>
27+
/// Represents a user with a first name, last name, and username.
28+
/// </summary>
29+
public class User
30+
{
31+
/// <summary>
32+
/// Gets or sets the user's first name.
33+
/// </summary>
34+
public string FirstName { get; set; }
35+
36+
/// <summary>
37+
/// Gets or sets the user's last name.
38+
/// </summary>
39+
public string LastName { get; set; }
40+
41+
/// <summary>
42+
/// Gets or sets the user's username.
43+
/// </summary>
44+
public string Username { get; set; }
45+
}
46+
"""
47+
48+
var body: some View {
49+
@Bindable var viewModelBindable = viewModel
50+
51+
VStack {
52+
Form {
53+
Section("Prompt") {
54+
TextField("Prompt", text: $prompt)
55+
}
56+
57+
Section("Prediction") {
58+
Text(prediction)
59+
}
60+
61+
Section("Response") {
62+
Text(response)
63+
}
64+
65+
Section("Prediction Section") {
66+
Text("Accepted Prediction Tokens")
67+
.badge(acceptedPredictionTokens.formatted())
68+
69+
Text("Rejected Prediction Tokens")
70+
.badge(rejectedPredictionTokens.formatted())
71+
}
72+
73+
UsageSection(inputTokens: inputTokens, outputTokens: outputTokens, totalTokens: totalTokens)
74+
}
75+
76+
VStack {
77+
SendButton(stream: viewModel.stream, onSend: onSend, onStream: onStream)
78+
}
79+
}
80+
.toolbar {
81+
ToolbarItem(placement: .principal) {
82+
NavigationTitle("Predicted Outputs")
83+
}
84+
85+
ToolbarItem(placement: .primaryAction) {
86+
Button("Preferences", systemImage: "gearshape", action: { isPreferencesPresented.toggle() })
87+
}
88+
}
89+
.sheet(isPresented: $isPreferencesPresented) {
90+
PreferencesView()
91+
}
92+
.onAppear {
93+
viewModel.setup(for: provider)
94+
}
95+
.onDisappear {
96+
viewModel.selectedModel = ""
97+
}
98+
}
99+
100+
private func onSend() {
101+
clear()
102+
103+
let messages = [
104+
ChatMessage(role: .user, content: prompt),
105+
ChatMessage(role: .user, content: prediction)
106+
]
107+
108+
let options = ChatOptions(
109+
prediction: .init(type: .content, content: [.init(type: "text", text: prediction)]),
110+
temperature: viewModel.temperature
111+
)
112+
113+
Task {
114+
do {
115+
let completion = try await viewModel.chat.send(model: viewModel.selectedModel, messages: messages, options: options)
116+
117+
if let content = completion.choices.first?.message.content {
118+
self.response = content
119+
}
120+
121+
if let usage = completion.usage {
122+
if let completionTokensDetails = usage.completionTokensDetails {
123+
self.acceptedPredictionTokens = completionTokensDetails.acceptedPredictionTokens
124+
self.rejectedPredictionTokens = completionTokensDetails.rejectedPredictionTokens
125+
}
126+
127+
self.inputTokens = usage.promptTokens
128+
self.outputTokens = usage.completionTokens
129+
self.totalTokens = usage.totalTokens
130+
}
131+
} catch {
132+
print(String(describing: error))
133+
}
134+
}
135+
}
136+
137+
private func onStream() {
138+
clear()
139+
140+
let messages = [
141+
ChatMessage(role: .user, content: prompt),
142+
ChatMessage(role: .user, content: prediction)
143+
]
144+
145+
let options = ChatOptions(
146+
prediction: .init(type: .content, content: prediction),
147+
temperature: viewModel.temperature
148+
)
149+
150+
Task {
151+
do {
152+
for try await chunk in viewModel.chat.stream(model: viewModel.selectedModel, messages: messages, options: options) {
153+
if let content = chunk.choices.first?.delta.content {
154+
self.response += content
155+
}
156+
157+
if let usage = chunk.usage {
158+
if let completionTokensDetails = usage.completionTokensDetails {
159+
self.acceptedPredictionTokens = completionTokensDetails.acceptedPredictionTokens
160+
self.rejectedPredictionTokens = completionTokensDetails.rejectedPredictionTokens
161+
}
162+
163+
self.inputTokens = usage.promptTokens ?? 0
164+
self.outputTokens = usage.completionTokens ?? 0
165+
self.totalTokens = usage.totalTokens ?? 0
166+
}
167+
}
168+
} catch {
169+
print(String(describing: error))
170+
}
171+
}
172+
}
173+
174+
private func clear() {
175+
response = ""
176+
acceptedPredictionTokens = 0
177+
rejectedPredictionTokens = 0
178+
inputTokens = 0
179+
outputTokens = 0
180+
totalTokens = 0
181+
}
182+
}

README.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,54 @@ Task {
199199

200200
To learn more about function calling, check out the [OpenAI documentation](https://platform.openai.com/docs/guides/function-calling).
201201

202+
#### Predicted Outputs
203+
204+
```swift
205+
private let code = """
206+
/// <summary>
207+
/// Represents a user with a first name, last name, and username.
208+
/// </summary>
209+
public class User
210+
{
211+
/// <summary>
212+
/// Gets or sets the user's first name.
213+
/// </summary>
214+
public string FirstName { get; set; }
215+
216+
/// <summary>
217+
/// Gets or sets the user's last name.
218+
/// </summary>
219+
public string LastName { get; set; }
220+
221+
/// <summary>
222+
/// Gets or sets the user's username.
223+
/// </summary>
224+
public string Username { get; set; }
225+
}
226+
"""
227+
228+
let messages = [
229+
ChatMessage(role: .user, content: "Replace the Username property with an Email property. Respond only with code, and with no markdown formatting."),
230+
ChatMessage(role: .user, content: code)
231+
]
232+
233+
let options = ChatOptions(
234+
prediction: .init(type: .content, content: code)
235+
)
236+
237+
Task {
238+
do {
239+
let completion = try await chat.send(model: "gpt-4o", messages: messages, options: options)
240+
241+
print(completion.choices.first?.message.content ?? "")
242+
} catch {
243+
print(String(describing: error))
244+
}
245+
}
246+
```
247+
248+
To learn more about predicted outputs, check out the [OpenAI documentation](https://platform.openai.com/docs/guides/latency-optimization#use-predicted-outputs).
249+
202250
#### Structured Outputs
203251

204252
```swift

Sources/LLMChatOpenAI/ChatCompletion.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,11 +161,20 @@ public struct ChatCompletion: Decodable, Sendable {
161161
public let promptTokensDetails: PromptTokensDetails?
162162

163163
public struct CompletionTokensDetails: Decodable, Sendable {
164+
/// When using Predicted Outputs, the number of tokens in the prediction that appeared in the completion.
165+
public let acceptedPredictionTokens: Int
166+
167+
/// When using Predicted Outputs, the number of tokens in the prediction that did not appear in the completion.
168+
/// However, like reasoning tokens, these tokens are still counted in the total completion tokens for purposes of billing, output, and context window limits.
169+
public let rejectedPredictionTokens: Int
170+
164171
/// Tokens generated by the model for reasoning.
165172
public let reasoningTokens: Int
166173

167174
private enum CodingKeys: String, CodingKey {
175+
case acceptedPredictionTokens = "accepted_prediction_tokens"
168176
case reasoningTokens = "reasoning_tokens"
177+
case rejectedPredictionTokens = "rejected_prediction_tokens"
169178
}
170179
}
171180

Sources/LLMChatOpenAI/ChatCompletionChunk.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,11 +153,20 @@ public struct ChatCompletionChunk: Decodable, Sendable {
153153
public let promptTokensDetails: PromptTokensDetails?
154154

155155
public struct CompletionTokensDetails: Decodable, Sendable {
156+
/// When using Predicted Outputs, the number of tokens in the prediction that appeared in the completion.
157+
public let acceptedPredictionTokens: Int
158+
159+
/// When using Predicted Outputs, the number of tokens in the prediction that did not appear in the completion.
160+
/// However, like reasoning tokens, these tokens are still counted in the total completion tokens for purposes of billing, output, and context window limits.
161+
public let rejectedPredictionTokens: Int
162+
156163
/// Tokens generated by the model for reasoning.
157164
public let reasoningTokens: Int
158165

159166
private enum CodingKeys: String, CodingKey {
167+
case acceptedPredictionTokens = "accepted_prediction_tokens"
160168
case reasoningTokens = "reasoning_tokens"
169+
case rejectedPredictionTokens = "rejected_prediction_tokens"
161170
}
162171
}
163172

0 commit comments

Comments
 (0)