Skip to content

Commit 8cfa454

Browse files
Predicted outputs
1 parent 77418b0 commit 8cfa454

File tree

8 files changed

+286
-18
lines changed

8 files changed

+286
-18
lines changed

Examples/SwiftOpenAIExample/SwiftOpenAIExample.xcodeproj/project.pbxproj

+12
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969
7BBE7EAB2B02E8FC0096A693 /* ChatMessageDisplayModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BBE7EAA2B02E8FC0096A693 /* ChatMessageDisplayModel.swift */; };
7070
7BBE7EDE2B03718E0096A693 /* ChatFunctionCallProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BBE7EDD2B03718E0096A693 /* ChatFunctionCallProvider.swift */; };
7171
7BBE7EE02B0372550096A693 /* ChatFunctionCallDemoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BBE7EDF2B0372550096A693 /* ChatFunctionCallDemoView.swift */; };
72+
7BE802592D2878170080E06A /* ChatPredictedOutputDemoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BE802582D2878170080E06A /* ChatPredictedOutputDemoView.swift */; };
7273
7BE9A5AF2B0B33E600CE8103 /* SwiftOpenAIExampleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BA788DE2AE23A49008825D5 /* SwiftOpenAIExampleTests.swift */; };
7374
/* End PBXBuildFile section */
7475

@@ -156,6 +157,7 @@
156157
7BBE7EAA2B02E8FC0096A693 /* ChatMessageDisplayModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatMessageDisplayModel.swift; sourceTree = "<group>"; };
157158
7BBE7EDD2B03718E0096A693 /* ChatFunctionCallProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatFunctionCallProvider.swift; sourceTree = "<group>"; };
158159
7BBE7EDF2B0372550096A693 /* ChatFunctionCallDemoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatFunctionCallDemoView.swift; sourceTree = "<group>"; };
160+
7BE802582D2878170080E06A /* ChatPredictedOutputDemoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatPredictedOutputDemoView.swift; sourceTree = "<group>"; };
159161
/* End PBXFileReference section */
160162

161163
/* Begin PBXFrameworksBuildPhase section */
@@ -378,6 +380,7 @@
378380
isa = PBXGroup;
379381
children = (
380382
7BA788CC2AE23A48008825D5 /* SwiftOpenAIExampleApp.swift */,
383+
7BE802572D2877D30080E06A /* PredictedOutputsDemo */,
381384
7B50DD292C2A9D1D0070A64D /* LocalChatDemo */,
382385
7B99C2E52C0718CD00E701B3 /* Files */,
383386
7B7239AF2AF9FF1D00646679 /* SharedModels */,
@@ -480,6 +483,14 @@
480483
path = Completion;
481484
sourceTree = "<group>";
482485
};
486+
7BE802572D2877D30080E06A /* PredictedOutputsDemo */ = {
487+
isa = PBXGroup;
488+
children = (
489+
7BE802582D2878170080E06A /* ChatPredictedOutputDemoView.swift */,
490+
);
491+
path = PredictedOutputsDemo;
492+
sourceTree = "<group>";
493+
};
483494
/* End PBXGroup section */
484495

485496
/* Begin PBXNativeTarget section */
@@ -620,6 +631,7 @@
620631
buildActionMask = 2147483647;
621632
files = (
622633
7BBE7EA92B02E8E50096A693 /* ChatMessageView.swift in Sources */,
634+
7BE802592D2878170080E06A /* ChatPredictedOutputDemoView.swift in Sources */,
623635
7B7239AE2AF9FF0000646679 /* ChatFunctionsCallStreamProvider.swift in Sources */,
624636
7B436BA12AE25958003CE281 /* ChatProvider.swift in Sources */,
625637
7B436BC32AE7B027003CE281 /* ModerationDemoView.swift in Sources */,

Examples/SwiftOpenAIExample/SwiftOpenAIExample/ChatDemo/ChatProvider.swift

+6-1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import SwiftOpenAI
1616
var messages: [String] = []
1717
var errorMessage: String = ""
1818
var message: String = ""
19+
var usage: ChatUsage?
1920

2021
init(service: OpenAIService) {
2122
self.service = service
@@ -25,10 +26,14 @@ import SwiftOpenAI
2526
parameters: ChatCompletionParameters) async throws
2627
{
2728
do {
28-
let choices = try await service.startChat(parameters: parameters).choices
29+
let response = try await service.startChat(parameters: parameters)
30+
let choices = response.choices
31+
let chatUsage = response.usage
2932
let logprobs = choices.compactMap(\.logprobs)
3033
dump(logprobs)
3134
self.messages = choices.compactMap(\.message.content)
35+
dump(chatUsage)
36+
self.usage = chatUsage
3237
} catch APIError.responseUnsuccessful(let description, let statusCode) {
3338
self.errorMessage = "Network error with status code: \(statusCode) and description: \(description)"
3439
} catch {

Examples/SwiftOpenAIExample/SwiftOpenAIExample/OptionsListView.swift

+3
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ struct OptionsListView: View {
2020
enum APIOption: String, CaseIterable, Identifiable {
2121
case audio = "Audio"
2222
case chat = "Chat"
23+
case chatPredictedOutput = "Chat Predicted Output"
2324
case localChat = "Local Chat" // Ollama
2425
case vision = "Vision"
2526
case embeddings = "Embeddings"
@@ -51,6 +52,8 @@ struct OptionsListView: View {
5152
AudioDemoView(service: openAIService)
5253
case .chat:
5354
ChatDemoView(service: openAIService)
55+
case .chatPredictedOutput:
56+
ChatPredictedOutputDemoView(service: openAIService)
5457
case .vision:
5558
ChatVisionDemoView(service: openAIService)
5659
case .embeddings:
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
//
2+
// ChatPredictedOutputDemoView.swift
3+
// SwiftOpenAIExample
4+
//
5+
// Created by James Rochabrun on 1/3/25.
6+
//
7+
8+
import Foundation
9+
import SwiftUI
10+
import SwiftOpenAI
11+
12+
/// https://platform.openai.com/docs/guides/predicted-outputs
13+
struct ChatPredictedOutputDemoView: View {
14+
15+
@State private var chatProvider: ChatProvider
16+
@State private var isLoading = false
17+
@State private var prompt = ""
18+
19+
init(service: OpenAIService) {
20+
chatProvider = ChatProvider(service: service)
21+
}
22+
23+
var body: some View {
24+
ScrollView {
25+
VStack {
26+
textArea
27+
Text(chatProvider.errorMessage)
28+
.foregroundColor(.red)
29+
chatCompletionResultView
30+
}
31+
}
32+
.overlay(
33+
Group {
34+
if isLoading {
35+
ProgressView()
36+
} else {
37+
EmptyView()
38+
}
39+
}
40+
)
41+
}
42+
43+
var textArea: some View {
44+
HStack(spacing: 4) {
45+
TextField("Enter prompt", text: $prompt, axis: .vertical)
46+
.textFieldStyle(.roundedBorder)
47+
.padding()
48+
Button {
49+
Task {
50+
isLoading = true
51+
defer { isLoading = false } // ensure isLoading is set to false when the
52+
53+
let content: ChatCompletionParameters.Message.ContentType = .text(prompt)
54+
prompt = ""
55+
let parameters = ChatCompletionParameters(
56+
messages: [
57+
.init(role: .system, content: .text(systemMessage)),
58+
.init(role: .user, content: content),
59+
.init(role: .user, content: .text(predictedCode))], // Sending the predicted code as another user message.
60+
model: .gpt4o,
61+
prediction: .init(content: .text(predictedCode)))
62+
try await chatProvider.startChat(parameters: parameters)
63+
64+
}
65+
} label: {
66+
Image(systemName: "paperplane")
67+
}
68+
.buttonStyle(.bordered)
69+
}
70+
.padding()
71+
}
72+
73+
/// stream = `false`
74+
var chatCompletionResultView: some View {
75+
ForEach(Array(chatProvider.messages.enumerated()), id: \.offset) { idx, val in
76+
VStack(spacing: 0) {
77+
Text("\(val)")
78+
}
79+
}
80+
}
81+
}
82+
83+
let systemMessage = """
84+
You are a code editor assistant. I only output code without any explanations, commentary, or additional text. I follow these rules:
85+
86+
1. Respond with code only, never any text or explanations
87+
2. Use appropriate syntax highlighting/formatting
88+
3. If the code needs to be modified/improved, output the complete updated code
89+
4. Do not include caveats, introductions, or commentary
90+
5. Do not ask questions or solicit feedback
91+
6. Do not explain what changes were made
92+
7. Assume the user knows what they want and will review the code themselves
93+
"""
94+
95+
let predictedCode = """
96+
struct ChatPredictedOutputDemoView: View {
97+
98+
@State private var chatProvider: ChatProvider
99+
@State private var isLoading = false
100+
@State private var prompt = ""
101+
102+
init(service: OpenAIService) {
103+
chatProvider = ChatProvider(service: service)
104+
}
105+
106+
var body: some View {
107+
ScrollView {
108+
VStack {
109+
textArea
110+
Text(chatProvider.errorMessage)
111+
.foregroundColor(.red)
112+
streamedChatResultView
113+
}
114+
}
115+
.overlay(
116+
Group {
117+
if isLoading {
118+
ProgressView()
119+
} else {
120+
EmptyView()
121+
}
122+
}
123+
)
124+
}
125+
126+
var textArea: some View {
127+
HStack(spacing: 4) {
128+
TextField("Enter prompt", text: $prompt, axis: .vertical)
129+
.textFieldStyle(.roundedBorder)
130+
.padding()
131+
Button {
132+
Task {
133+
isLoading = true
134+
defer { isLoading = false } // ensure isLoading is set to false when the
135+
136+
let content: ChatCompletionParameters.Message.ContentType = .text(prompt)
137+
prompt = ""
138+
let parameters = ChatCompletionParameters(
139+
messages: [.init(
140+
role: .user,
141+
content: content)],
142+
model: .gpt4o)
143+
}
144+
} label: {
145+
Image(systemName: "paperplane")
146+
}
147+
.buttonStyle(.bordered)
148+
}
149+
.padding()
150+
}
151+
152+
/// stream = `true`
153+
var streamedChatResultView: some View {
154+
VStack {
155+
Button("Cancel stream") {
156+
chatProvider.cancelStream()
157+
}
158+
Text(chatProvider.message)
159+
}
160+
}
161+
}
162+
"""

Sources/OpenAI/Public/Parameters/Chat/ChatCompletionParameters.swift

+40
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ public struct ChatCompletionParameters: Encodable {
5959
///The gpt-4o-audio-preview model can also be used to [generate audio](https://platform.openai.com/docs/guides/audio). To request that this model generate both text and audio responses, you can use:
6060
/// ["text", "audio"]
6161
public var modalities: [String]?
62+
/// Configuration for a [Predicted Output](https://platform.openai.com/docs/guides/predicted-outputs), which can greatly improve response times when large parts of the model response are known ahead of time. This is most common when you are regenerating a file with only minor changes to most of the content.
63+
public var prediction: Prediction?
6264
/// Parameters for audio output. Required when audio output is requested with modalities: ["audio"]. [Learn more.](https://platform.openai.com/docs/guides/audio)
6365
public var audio: Audio?
6466
/// Number between -2.0 and 2.0. Positive values penalize new tokens based on whether they appear in the text so far, increasing the model's likelihood to talk about new topics. Defaults to 0
@@ -380,6 +382,41 @@ public struct ChatCompletionParameters: Encodable {
380382
}
381383
}
382384

385+
public struct Prediction: Encodable {
386+
public let type: String
387+
public let content: PredictionContent
388+
389+
public enum PredictionContent: Encodable {
390+
case text(String)
391+
case contentArray([ContentPart])
392+
393+
public func encode(to encoder: Encoder) throws {
394+
var container = encoder.singleValueContainer()
395+
switch self {
396+
case .text(let text):
397+
try container.encode(text)
398+
case .contentArray(let parts):
399+
try container.encode(parts)
400+
}
401+
}
402+
}
403+
404+
public struct ContentPart: Encodable {
405+
public let type: String
406+
public let text: String
407+
408+
public init(type: String, text: String) {
409+
self.type = type
410+
self.text = text
411+
}
412+
}
413+
414+
public init(content: PredictionContent, type: String = "content") {
415+
self.type = type
416+
self.content = content
417+
}
418+
}
419+
383420
public enum ReasoningEffort: String, Encodable {
384421
case low
385422
case medium
@@ -405,6 +442,7 @@ public struct ChatCompletionParameters: Encodable {
405442
case maCompletionTokens = "max_completion_tokens"
406443
case n
407444
case modalities
445+
case prediction
408446
case audio
409447
case responseFormat = "response_format"
410448
case presencePenalty = "presence_penalty"
@@ -436,6 +474,7 @@ public struct ChatCompletionParameters: Encodable {
436474
maxTokens: Int? = nil,
437475
n: Int? = nil,
438476
modalities: [String]? = nil,
477+
prediction: Prediction? = nil,
439478
audio: Audio? = nil,
440479
responseFormat: ResponseFormat? = nil,
441480
presencePenalty: Double? = nil,
@@ -464,6 +503,7 @@ public struct ChatCompletionParameters: Encodable {
464503
self.maxTokens = maxTokens
465504
self.n = n
466505
self.modalities = modalities
506+
self.prediction = prediction
467507
self.audio = audio
468508
self.responseFormat = responseFormat
469509
self.presencePenalty = presencePenalty

Sources/OpenAI/Public/ResponseModels/Chat/ChatCompletionChunkObject.swift

+3
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ public struct ChatCompletionChunkObject: Decodable {
2525
public let systemFingerprint: String?
2626
/// The object type, which is always chat.completion.chunk.
2727
public let object: String
28+
/// An optional field that will only be present when you set stream_options: {"include_usage": true} in your request. When present, it contains a null value except for the last chunk which contains the token usage statistics for the entire request.
29+
public let usage: ChatUsage?
2830

2931
public struct ChatChoice: Decodable {
3032

@@ -114,5 +116,6 @@ public struct ChatCompletionChunkObject: Decodable {
114116
case serviceTier = "service_tier"
115117
case systemFingerprint = "system_fingerprint"
116118
case object
119+
case usage
117120
}
118121
}

Sources/OpenAI/Public/ResponseModels/Chat/ChatCompletionObject.swift

+1-17
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ public struct ChatCompletionObject: Decodable {
2626
/// The object type, which is always chat.completion.
2727
public let object: String
2828
/// Usage statistics for the completion request.
29-
public let usage: ChatUsage
29+
public let usage: ChatUsage?
3030

3131
public struct ChatChoice: Decodable {
3232

@@ -139,20 +139,4 @@ public struct ChatCompletionObject: Decodable {
139139
case object
140140
case usage
141141
}
142-
143-
public struct ChatUsage: Decodable {
144-
145-
/// Number of tokens in the generated completion.
146-
public let completionTokens: Int
147-
/// Number of tokens in the prompt.
148-
public let promptTokens: Int
149-
/// Total number of tokens used in the request (prompt + completion).
150-
public let totalTokens: Int
151-
152-
enum CodingKeys: String, CodingKey {
153-
case completionTokens = "completion_tokens"
154-
case promptTokens = "prompt_tokens"
155-
case totalTokens = "total_tokens"
156-
}
157-
}
158142
}

0 commit comments

Comments
 (0)