Skip to content

Commit 3c224f3

Browse files
committed
Fix streaming
1 parent 0da7b83 commit 3c224f3

File tree

6 files changed

+51
-12
lines changed

6 files changed

+51
-12
lines changed

Demo/App/APIProvidedView.swift

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,26 +26,28 @@ struct APIProvidedView: View {
2626
idProvider: @escaping () -> String
2727
) {
2828
self._apiKey = apiKey
29+
30+
let client = APIProvidedView.makeClient(apiKey: apiKey.wrappedValue)
2931
self._chatStore = StateObject(
3032
wrappedValue: ChatStore(
31-
openAIClient: OpenAI(apiToken: apiKey.wrappedValue),
33+
openAIClient: client,
3234
idProvider: idProvider
3335
)
3436
)
3537
self._imageStore = StateObject(
3638
wrappedValue: ImageStore(
37-
openAIClient: OpenAI(apiToken: apiKey.wrappedValue)
39+
openAIClient: client
3840
)
3941
)
4042
self._assistantStore = StateObject(
4143
wrappedValue: AssistantStore(
42-
openAIClient: OpenAI(apiToken: apiKey.wrappedValue),
44+
openAIClient: client,
4345
idProvider: idProvider
4446
)
4547
)
4648
self._miscStore = StateObject(
4749
wrappedValue: MiscStore(
48-
openAIClient: OpenAI(apiToken: apiKey.wrappedValue)
50+
openAIClient: client
4951
)
5052
)
5153
}
@@ -58,11 +60,15 @@ struct APIProvidedView: View {
5860
miscStore: miscStore
5961
)
6062
.onChange(of: apiKey) { newApiKey in
61-
let client = OpenAI(apiToken: newApiKey)
63+
let client = APIProvidedView.makeClient(apiKey: newApiKey)
6264
chatStore.openAIClient = client
6365
imageStore.openAIClient = client
6466
assistantStore.openAIClient = client
6567
miscStore.openAIClient = client
6668
}
6769
}
70+
71+
private static func makeClient(apiKey: String) -> OpenAIProtocol {
72+
OpenAI(apiToken: apiKey)
73+
}
6874
}

Sources/OpenAI/Private/Streaming/ServerSentEventsStreamParser.swift

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,10 @@ final class ServerSentEventsStreamParser: @unchecked Sendable {
8888
for i in 0..<streamData.count {
8989
let currentChar = streamData[i]
9090
if currentChar == lf {
91-
// The description above basically says that "if the char is LF - it's end if line, regardless what precedes or follows it
92-
lines.append(streamData.subdata(in: lineBeginningIndex..<i))
91+
// The description above basically says that "if the char is LF - it's end if line, regardless what precedes or follows it.
92+
// But if previous chat was CR we should exclude that, too
93+
let lineEndIndex = previousCharacter == cr ? i - 1 : i
94+
lines.append(streamData.subdata(in: lineBeginningIndex..<lineEndIndex))
9395
lineBeginningIndex = i + 1
9496
} else if currentChar == cr {
9597
// The description above basically says that "CR is not end of line only if followed by LF"
@@ -103,7 +105,7 @@ final class ServerSentEventsStreamParser: @unchecked Sendable {
103105
// Simply skipping
104106
}
105107

106-
previousCharacter = 0
108+
previousCharacter = currentChar
107109
}
108110

109111
if lineBeginningIndex < streamData.count - 1 {

Sources/OpenAI/Private/Streaming/StreamingSession.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,8 @@ final class StreamingSession<Interpreter: StreamInterpreter>: NSObject, Identifi
7070
}
7171

7272
func urlSession(
73-
_ session: URLSession,
74-
dataTask: URLSessionDataTask,
73+
_ session: URLSessionProtocol,
74+
dataTask: URLSessionDataTaskProtocol,
7575
didReceive response: URLResponse,
7676
completionHandler: @escaping (URLSession.ResponseDisposition) -> Void
7777
) {

Sources/OpenAI/Private/URLSessionDataDelegateForwarder.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,12 @@ final class URLSessionDataDelegateForwarder: NSObject, URLSessionDataDelegate {
2525
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
2626
target.urlSession(session, dataTask: dataTask, didReceive: data)
2727
}
28+
29+
func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
30+
target.urlSession(session, didReceive: challenge, completionHandler: completionHandler)
31+
}
32+
33+
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) {
34+
target.urlSession(session, dataTask: dataTask, didReceive: response, completionHandler: completionHandler)
35+
}
2836
}

Sources/OpenAI/Private/URLSessionDelegateProtocol.swift

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,21 @@ import Foundation
99

1010
protocol URLSessionDelegateProtocol: Sendable { // Sendable to make a better match with URLSessionDelegate, it's sendable too
1111
func urlSession(_ session: URLSessionProtocol, task: URLSessionTaskProtocol, didCompleteWithError error: Error?)
12+
13+
func urlSession(
14+
_ session: URLSession,
15+
didReceive challenge: URLAuthenticationChallenge,
16+
completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void
17+
)
1218
}
1319

1420
protocol URLSessionDataDelegateProtocol: URLSessionDelegateProtocol {
1521
func urlSession(_ session: URLSessionProtocol, dataTask: URLSessionDataTaskProtocol, didReceive data: Data)
22+
23+
func urlSession(
24+
_ session: URLSessionProtocol,
25+
dataTask: URLSessionDataTaskProtocol,
26+
didReceive response: URLResponse,
27+
completionHandler: @escaping (URLSession.ResponseDisposition) -> Void
28+
)
1629
}

Tests/OpenAITests/ChatGPTGeneratedSSEParserTests.swift renamed to Tests/OpenAITests/ServerSentEventsStreamParserTests.swift

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
//
2-
// ChatGPTGeneratedSSEParserTests.swift
2+
// ServerSentEventsStreamParserTests.swift
33
// OpenAI
44
//
55
// Created by Oleksii Nezhyborets on 04.04.2025.
@@ -8,7 +8,7 @@
88
import XCTest
99
@testable import OpenAI
1010

11-
final class ChatGPTGeneratedSSEParserTests: XCTestCase {
11+
final class ServerSentEventsStreamParserTests: XCTestCase {
1212
private let parser = ServerSentEventsStreamParser()
1313

1414
func testSingleDataLine() {
@@ -87,6 +87,16 @@ final class ChatGPTGeneratedSSEParserTests: XCTestCase {
8787
XCTAssertEqual(events[0].decodedData, "real")
8888
}
8989

90+
// Perplexity is sending such line-end indicators:
91+
// \r\n\r\n
92+
// and it seems to be valid (per spec), so it should work
93+
func testCrLfCrLf() {
94+
let input = "data: value1\r\n\r\ndata: value2\n\n"
95+
let events = parse(input)
96+
XCTAssertEqual(events[0].decodedData, "value1")
97+
XCTAssertEqual(events[1].decodedData, "value2")
98+
}
99+
90100
// Helper
91101
func parse(_ raw: String) -> [ServerSentEventsStreamParser.Event] {
92102
let parser = ServerSentEventsStreamParser()

0 commit comments

Comments
 (0)