Skip to content

Commit 7eef412

Browse files
authored
Merge pull request #318 from MacPaw/317-perplexity-fails-decoding-starting-with-040-version
Fix streaming
2 parents 0da7b83 + ee1eeba commit 7eef412

File tree

6 files changed

+55
-12
lines changed

6 files changed

+55
-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: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,27 @@
77

88
import Foundation
99

10+
#if canImport(FoundationNetworking)
11+
import FoundationNetworking
12+
#endif
13+
1014
protocol URLSessionDelegateProtocol: Sendable { // Sendable to make a better match with URLSessionDelegate, it's sendable too
1115
func urlSession(_ session: URLSessionProtocol, task: URLSessionTaskProtocol, didCompleteWithError error: Error?)
16+
17+
func urlSession(
18+
_ session: URLSession,
19+
didReceive challenge: URLAuthenticationChallenge,
20+
completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void
21+
)
1222
}
1323

1424
protocol URLSessionDataDelegateProtocol: URLSessionDelegateProtocol {
1525
func urlSession(_ session: URLSessionProtocol, dataTask: URLSessionDataTaskProtocol, didReceive data: Data)
26+
27+
func urlSession(
28+
_ session: URLSessionProtocol,
29+
dataTask: URLSessionDataTaskProtocol,
30+
didReceive response: URLResponse,
31+
completionHandler: @escaping (URLSession.ResponseDisposition) -> Void
32+
)
1633
}

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)