Skip to content

Commit 47cf112

Browse files
Add comprehensive Response API streaming support and fix conversation continuity
## Summary - Added full streaming support for Response API with 40+ event types in ResponseStreamEvent - Fixed conversation state management by adding `id` field to InputMessage for response tracking - Implemented ResponseStreamProvider demo showing real-time streaming with conversation history - Updated README with comprehensive streaming documentation and examples ## Changes - **Response API Streaming**: - Added ResponseStreamEvent enum with all streaming event types - Implemented stream event parsing for text deltas, function calls, tool usage, etc. - Added support for reasoning summaries, web search, file search events - **Conversation State Fix**: - Added `id` field to InputMessage to support response IDs in conversation history - Fixed 400 error when sending follow-up messages with previous assistant responses - Simplified assistant message handling in conversation arrays - **Demo Implementation**: - Created ResponseStreamProvider with full streaming capabilities - Added ResponseStreamDemoView showing real-time UI updates - Implemented message accumulation and conversation state management - **Documentation**: - Added comprehensive streaming section to README - Documented all new Response API types and structures - Included multiple code examples for various streaming scenarios 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent 9827a40 commit 47cf112

File tree

15 files changed

+1785
-1403
lines changed

15 files changed

+1785
-1403
lines changed

Examples/SwiftOpenAIExample/SwiftOpenAIExample/OptionsListView.swift

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import SwiftOpenAI
99
import SwiftUI
1010

1111
struct OptionsListView: View {
12-
12+
1313
/// https://platform.openai.com/docs/api-reference
1414
enum APIOption: String, CaseIterable, Identifiable {
1515
case audio = "Audio"
@@ -31,14 +31,14 @@ struct OptionsListView: View {
3131
case configureAssistant = "Configure Assistant"
3232
case realTimeAPI = "Real time API"
3333
case responseStream = "Response Stream Demo"
34-
34+
3535
var id: String { rawValue }
3636
}
37-
37+
3838
var openAIService: OpenAIService
39-
39+
4040
var options: [APIOption]
41-
41+
4242
var body: some View {
4343
List(options, id: \.self, selection: $selection) { option in
4444
Text(option.rawValue)
@@ -91,7 +91,7 @@ struct OptionsListView: View {
9191
}
9292
}
9393
}
94-
94+
9595
@State private var selection: APIOption? = nil
96-
96+
9797
}

Examples/SwiftOpenAIExample/SwiftOpenAIExample/ResponseAPIDemo/ResponseStreamDemoView.swift

Lines changed: 43 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -5,25 +5,24 @@
55
// Created by James Rochabrun on 6/7/25.
66
//
77

8-
import SwiftUI
98
import SwiftOpenAI
9+
import SwiftUI
10+
11+
// MARK: - ResponseStreamDemoView
1012

1113
struct ResponseStreamDemoView: View {
12-
13-
@State private var provider: ResponseStreamProvider
14-
@State private var inputText = ""
15-
@FocusState private var isInputFocused: Bool
16-
@Environment(\.colorScheme) var colorScheme
17-
14+
1815
init(service: OpenAIService) {
19-
self._provider = State(initialValue: ResponseStreamProvider(service: service))
16+
_provider = State(initialValue: ResponseStreamProvider(service: service))
2017
}
21-
18+
19+
@Environment(\.colorScheme) var colorScheme
20+
2221
var body: some View {
2322
VStack(spacing: 0) {
2423
// Header
2524
headerView
26-
25+
2726
// Messages
2827
ScrollViewReader { proxy in
2928
ScrollView {
@@ -32,7 +31,7 @@ struct ResponseStreamDemoView: View {
3231
MessageBubbleView(message: message)
3332
.id(message.id)
3433
}
35-
34+
3635
if provider.isStreaming {
3736
HStack {
3837
LoadingIndicatorView()
@@ -50,7 +49,7 @@ struct ResponseStreamDemoView: View {
5049
}
5150
}
5251
}
53-
52+
5453
// Error view
5554
if let error = provider.error {
5655
Text(error)
@@ -60,7 +59,7 @@ struct ResponseStreamDemoView: View {
6059
.padding(.vertical, 8)
6160
.background(Color.red.opacity(0.1))
6261
}
63-
62+
6463
// Input area
6564
inputArea
6665
}
@@ -75,18 +74,22 @@ struct ResponseStreamDemoView: View {
7574
}
7675
}
7776
}
78-
77+
78+
@State private var provider: ResponseStreamProvider
79+
@State private var inputText = ""
80+
@FocusState private var isInputFocused: Bool
81+
7982
// MARK: - Subviews
80-
83+
8184
private var headerView: some View {
8285
VStack(alignment: .leading, spacing: 8) {
8386
Text("Streaming Responses with Conversation State")
8487
.font(.headline)
85-
88+
8689
Text("This demo uses the Responses API with streaming to maintain conversation context across multiple turns.")
8790
.font(.caption)
8891
.foregroundColor(.secondary)
89-
92+
9093
if provider.messages.isEmpty {
9194
Label("Start a conversation below", systemImage: "bubble.left.and.bubble.right")
9295
.font(.caption)
@@ -98,7 +101,7 @@ struct ResponseStreamDemoView: View {
98101
.padding()
99102
.background(Color(UIColor.secondarySystemBackground))
100103
}
101-
104+
102105
private var inputArea: some View {
103106
HStack(spacing: 12) {
104107
TextField("Type a message...", text: $inputText, axis: .vertical)
@@ -109,7 +112,7 @@ struct ResponseStreamDemoView: View {
109112
.onSubmit {
110113
sendMessage()
111114
}
112-
115+
113116
Button(action: sendMessage) {
114117
Image(systemName: provider.isStreaming ? "stop.circle.fill" : "arrow.up.circle.fill")
115118
.font(.title2)
@@ -123,15 +126,12 @@ struct ResponseStreamDemoView: View {
123126
Rectangle()
124127
.frame(height: 1)
125128
.foregroundColor(Color(UIColor.separator)),
126-
alignment: .top
127-
)
129+
alignment: .top)
128130
}
129-
130-
// MARK: - Actions
131-
131+
132132
private func sendMessage() {
133133
guard !inputText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return }
134-
134+
135135
if provider.isStreaming {
136136
provider.stopStreaming()
137137
} else {
@@ -142,12 +142,12 @@ struct ResponseStreamDemoView: View {
142142
}
143143
}
144144

145-
// MARK: - Message Bubble View
145+
// MARK: - MessageBubbleView
146146

147147
struct MessageBubbleView: View {
148148
let message: ResponseStreamProvider.ResponseMessage
149149
@Environment(\.colorScheme) var colorScheme
150-
150+
151151
var body: some View {
152152
HStack {
153153
if message.role == .assistant {
@@ -156,8 +156,7 @@ struct MessageBubbleView: View {
156156
.cornerRadius(16)
157157
.overlay(
158158
RoundedRectangle(cornerRadius: 16)
159-
.stroke(borderColor, lineWidth: 1)
160-
)
159+
.stroke(borderColor, lineWidth: 1))
161160
Spacer(minLength: 60)
162161
} else {
163162
Spacer(minLength: 60)
@@ -168,10 +167,10 @@ struct MessageBubbleView: View {
168167
}
169168
}
170169
}
171-
170+
172171
private var messageContent: some View {
173172
VStack(alignment: .leading, spacing: 4) {
174-
if message.role == .assistant && message.isStreaming {
173+
if message.role == .assistant, message.isStreaming {
175174
HStack(spacing: 4) {
176175
Image(systemName: "dot.radiowaves.left.and.right")
177176
.font(.caption2)
@@ -181,12 +180,12 @@ struct MessageBubbleView: View {
181180
.foregroundColor(.secondary)
182181
}
183182
}
184-
183+
185184
Text(message.content.isEmpty && message.isStreaming ? " " : message.content)
186185
.padding(.horizontal, 12)
187186
.padding(.vertical, 8)
188-
189-
if message.role == .assistant && !message.isStreaming && message.responseId != nil {
187+
188+
if message.role == .assistant, !message.isStreaming, message.responseId != nil {
190189
Text("Response ID: \(String(message.responseId?.prefix(8) ?? ""))")
191190
.font(.caption2)
192191
.foregroundColor(.secondary)
@@ -195,28 +194,25 @@ struct MessageBubbleView: View {
195194
}
196195
}
197196
}
198-
197+
199198
private var backgroundGradient: some View {
200199
LinearGradient(
201200
gradient: Gradient(colors: [
202201
Color(UIColor.secondarySystemBackground),
203-
Color(UIColor.tertiarySystemBackground)
202+
Color(UIColor.tertiarySystemBackground),
204203
]),
205204
startPoint: .topLeading,
206-
endPoint: .bottomTrailing
207-
)
205+
endPoint: .bottomTrailing)
208206
}
209-
207+
210208
private var borderColor: Color {
211209
colorScheme == .dark ? Color.white.opacity(0.1) : Color.black.opacity(0.1)
212210
}
213211
}
214212

215-
// MARK: - Loading Indicator
213+
// MARK: - LoadingIndicatorView
216214

217215
struct LoadingIndicatorView: View {
218-
@State private var animationAmount = 0.0
219-
220216
var body: some View {
221217
ZStack {
222218
ForEach(0..<3) { index in
@@ -231,13 +227,15 @@ struct LoadingIndicatorView: View {
231227
.onAppear {
232228
withAnimation(
233229
.easeInOut(duration: 0.8)
234-
.repeatForever(autoreverses: true)
235-
) {
230+
.repeatForever(autoreverses: true))
231+
{
236232
animationAmount = 1
237233
}
238234
}
239235
}
240-
236+
237+
@State private var animationAmount = 0.0
238+
241239
private func animationScale(for index: Int) -> Double {
242240
let delay = Double(index) * 0.1
243241
let progress = (animationAmount + delay).truncatingRemainder(dividingBy: 1.0)

0 commit comments

Comments
 (0)