Skip to content

Commit a04afd4

Browse files
authored
[AI] Refactor stream parsing to skip intermediate failures (#15960)
1 parent 2378892 commit a04afd4

2 files changed

Lines changed: 47 additions & 7 deletions

File tree

FirebaseAI/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
# 12.12.0
2+
- [fixed] Updated `GenerativeModelSession` to handle intermediate JSON decoding
3+
failures when streaming structured data. (#15960)
4+
15
# 12.11.0
26
- [feature] **Public Preview**: Introduces `GenerativeModelSession` providing
37
APIs for generating structured data from Gemini via the same `@Generable` and

FirebaseAI/Sources/GenerativeModelSession.swift

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -454,6 +454,8 @@
454454
public extension GenerativeModelSession {
455455
/// An asynchronous sequence of snapshots of the model's response.
456456
struct ResponseStream<Content, PartialContent>: AsyncSequence {
457+
// TODO(#15962): Add unit tests for `ResponseStream`.
458+
457459
public typealias Element = Snapshot
458460

459461
/// A snapshot of the model's response at a point in time.
@@ -495,18 +497,52 @@
495497
@available(iOS 18.0, macOS 15.0, macCatalyst 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *)
496498
public mutating func next(isolation actor: isolated (any Actor)?) async throws
497499
-> Snapshot? {
498-
let rawResult = try await rawIterator.next(isolation: actor)
499-
return try process(rawResult)
500+
var lastDecodingError: Error? = nil
501+
502+
while let rawResult = try await rawIterator.next(isolation: actor) {
503+
do {
504+
// If it parses successfully, return the snapshot and discard any errors from previous
505+
// loop iterations.
506+
return try process(rawResult)
507+
} catch {
508+
// Intermediate failure (e.g., incomplete JSON that could not be parsed).
509+
// Hold onto the error and let the loop fetch the next chunk.
510+
lastDecodingError = error
511+
}
512+
}
513+
514+
// If the last chunk processed resulted in an error, throw it.
515+
if let lastDecodingError {
516+
throw lastDecodingError
517+
}
518+
519+
return nil
500520
}
501521

502522
public mutating func next() async throws -> Snapshot? {
503-
let rawResult = try await rawIterator.next()
504-
return try process(rawResult)
505-
}
523+
var lastDecodingError: Error? = nil
524+
525+
while let rawResult = try await rawIterator.next() {
526+
do {
527+
// If it parses successfully, return the snapshot and discard any errors from previous
528+
// loop iterations.
529+
return try process(rawResult)
530+
} catch {
531+
// Intermediate failure (e.g., incomplete JSON that could not be parsed).
532+
// Hold onto the error and let the loop fetch the next chunk.
533+
lastDecodingError = error
534+
}
535+
}
506536

507-
private func process(_ rawResult: RawResult?) throws -> Snapshot? {
508-
guard let rawResult else { return nil }
537+
// If the last chunk processed resulted in an error, throw it.
538+
if let lastDecodingError {
539+
throw lastDecodingError
540+
}
541+
542+
return nil
543+
}
509544

545+
private func process(_ rawResult: RawResult) throws -> Snapshot {
510546
let partialContent: PartialContent = try GenerativeModelSession
511547
.resolveContent(from: rawResult.rawContent)
512548

0 commit comments

Comments
 (0)