Skip to content

Commit 539733b

Browse files
committed
Merge remote-tracking branch 'upstream/main' into JesusRojass/#11903
2 parents b456bd8 + fa96c86 commit 539733b

4 files changed

Lines changed: 76 additions & 9 deletions

File tree

FirebaseAI/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
# Unreleased
2+
- [fixed] Fixed a decoding failure in `GenerateContentResponse` when the Vertex AI
3+
backend returns citation metadata with a missing `endIndex`. (#16328)
24
- [fixed] Fixed an issue where `generateContentStream` could stall indefinitely
35
on mid-stream network drops. (#16298)
46
- [fixed] Fixed a resource leak where background network requests would

FirebaseAI/Sources/GenerateContentResponse.swift

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -684,12 +684,10 @@ extension CitationMetadata: Decodable {
684684
public init(from decoder: any Decoder) throws {
685685
let container = try decoder.container(keyedBy: CodingKeys.self)
686686

687-
// Decode for Google API if `citationSources` key is present.
688-
if container.contains(.citationSources) {
689-
citations = try container.decode([Citation].self, forKey: .citationSources)
690-
} else { // Fallback to default Vertex AI decoding.
691-
citations = try container.decode([Citation].self, forKey: .citations)
692-
}
687+
let decodedCitations = try container.decodeIfPresent([Citation].self, forKey: .citationSources)
688+
?? container.decodeIfPresent([Citation].self, forKey: .citations)
689+
?? []
690+
citations = decodedCitations.filter { !$0.isEmpty }
693691
}
694692
}
695693

@@ -703,10 +701,19 @@ extension Citation: Decodable {
703701
case publicationDate
704702
}
705703

704+
var isEmpty: Bool {
705+
startIndex == 0 &&
706+
endIndex == 0 &&
707+
uri == nil &&
708+
title == nil &&
709+
license == nil &&
710+
publicationDate == nil
711+
}
712+
706713
public init(from decoder: any Decoder) throws {
707714
let container = try decoder.container(keyedBy: CodingKeys.self)
708715
startIndex = try container.decodeIfPresent(Int.self, forKey: .startIndex) ?? 0
709-
endIndex = try container.decode(Int.self, forKey: .endIndex)
716+
endIndex = try container.decodeIfPresent(Int.self, forKey: .endIndex) ?? startIndex
710717

711718
if let uri = try container.decodeIfPresent(String.self, forKey: .uri), !uri.isEmpty {
712719
self.uri = uri

FirebaseAI/Tests/Unit/Types/CitationTests.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,14 +99,16 @@ final class CitationTests: XCTestCase {
9999
XCTAssertNil(citation.publicationDate)
100100
}
101101

102-
func testDecodeCitation_missingEndIndex_throws() throws {
102+
func testDecodeCitation_missingEndIndex_success() throws {
103103
let json = """
104104
{
105105
"startIndex" : 10
106106
}
107107
"""
108108
let jsonData = try XCTUnwrap(json.data(using: .utf8))
109109

110-
XCTAssertThrowsError(try decoder.decode(Citation.self, from: jsonData))
110+
let citation = try decoder.decode(Citation.self, from: jsonData)
111+
XCTAssertEqual(citation.startIndex, 10)
112+
XCTAssertEqual(citation.endIndex, 10)
111113
}
112114
}

FirebaseAI/Tests/Unit/Types/GenerateContentResponseTests.swift

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,62 @@ final class GenerateContentResponseTests: XCTestCase {
343343
}
344344
}
345345

346+
func testDecodeGenerateContentResponse_missingCitationEndIndex_success() throws {
347+
let json = """
348+
{
349+
"candidates": [
350+
{
351+
"content": { "role": "model", "parts": [ { "text": "Some text." } ] },
352+
"finishReason": "STOP",
353+
"citationMetadata": {
354+
"citations": [
355+
{
356+
"uri": "https://example.com/source"
357+
}
358+
]
359+
}
360+
}
361+
]
362+
}
363+
"""
364+
let jsonData = try XCTUnwrap(json.data(using: .utf8))
365+
366+
let response = try jsonDecoder.decode(GenerateContentResponse.self, from: jsonData)
367+
368+
XCTAssertEqual(response.candidates.count, 1)
369+
let candidate = try XCTUnwrap(response.candidates.first)
370+
let citation = try XCTUnwrap(candidate.citationMetadata?.citations.first)
371+
XCTAssertEqual(citation.uri, "https://example.com/source")
372+
XCTAssertEqual(citation.endIndex, 0)
373+
XCTAssertEqual(citation.startIndex, 0)
374+
}
375+
376+
func testDecodeGenerateContentResponse_emptyCitation_success() throws {
377+
let json = """
378+
{
379+
"candidates": [
380+
{
381+
"content": { "role": "model", "parts": [ { "text": "Some text." } ] },
382+
"finishReason": "STOP",
383+
"citationMetadata": {
384+
"citations": [
385+
{}
386+
]
387+
}
388+
}
389+
]
390+
}
391+
"""
392+
let jsonData = try XCTUnwrap(json.data(using: .utf8))
393+
394+
let response = try jsonDecoder.decode(GenerateContentResponse.self, from: jsonData)
395+
396+
XCTAssertEqual(response.candidates.count, 1)
397+
let candidate = try XCTUnwrap(response.candidates.first)
398+
let citations = try XCTUnwrap(candidate.citationMetadata?.citations)
399+
XCTAssertTrue(citations.isEmpty)
400+
}
401+
346402
// MARK: - Candidate.isEmpty
347403

348404
func testCandidateIsEmpty_allEmpty_isTrue() throws {

0 commit comments

Comments
 (0)