Skip to content

Commit 61d6e91

Browse files
authored
[Vertex AI] Fix decoding ModalityTokenCount when tokenCount is 0 (#14747)
1 parent fb400f8 commit 61d6e91

File tree

5 files changed

+113
-7
lines changed

5 files changed

+113
-7
lines changed

FirebaseVertexAI/CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
# Unreleased
2+
- [fixed] Fixed `ModalityTokenCount` decoding when the `tokenCount` field is
3+
omitted; this occurs when the count is 0. (#14745)
4+
15
# 11.12.0
26
- [added] **Public Preview**: Added support for specifying response modalities
37
in `GenerationConfig`. This includes **public experimental** support for image

FirebaseVertexAI/Sources/ModalityTokenCount.swift

+14-1
Original file line numberDiff line numberDiff line change
@@ -57,5 +57,18 @@ public struct ContentModality: DecodableProtoEnum, Hashable, Sendable {
5757
VertexLog.MessageCode.generateContentResponseUnrecognizedContentModality
5858
}
5959

60+
// MARK: Codable Conformances
61+
6062
@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
61-
extension ModalityTokenCount: Decodable {}
63+
extension ModalityTokenCount: Decodable {
64+
enum CodingKeys: CodingKey {
65+
case modality
66+
case tokenCount
67+
}
68+
69+
public init(from decoder: any Decoder) throws {
70+
let container = try decoder.container(keyedBy: CodingKeys.self)
71+
modality = try container.decode(ContentModality.self, forKey: .modality)
72+
tokenCount = try container.decodeIfPresent(Int.self, forKey: .tokenCount) ?? 0
73+
}
74+
}

FirebaseVertexAI/Tests/TestApp/Tests/Integration/GenerateContentIntegrationTests.swift

+3-2
Original file line numberDiff line numberDiff line change
@@ -116,9 +116,10 @@ struct GenerateContentIntegrationTests {
116116

117117
@Test(arguments: [
118118
InstanceConfig.vertexV1Beta,
119-
// TODO(andrewheard): Prod config temporarily disabled due to backend issue.
119+
// TODO(andrewheard): Configs temporarily disabled due to backend issue.
120120
// InstanceConfig.developerV1Beta,
121-
InstanceConfig.developerV1BetaStaging, // Remove after re-enabling `developerV1Beta` config.
121+
// InstanceConfig.developerV1BetaStaging
122+
InstanceConfig.developerV1BetaSpark,
122123
])
123124
func generateImage(_ config: InstanceConfig) async throws {
124125
let generationConfig = GenerationConfig(

FirebaseVertexAI/Tests/TestApp/Tests/Utilities/InstanceConfig.swift

+4-4
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,9 @@ struct InstanceConfig {
5151
vertexV1Staging,
5252
vertexV1Beta,
5353
vertexV1BetaStaging,
54-
// TODO(andrewheard): Prod config temporarily disabled due to backend issue:
54+
// TODO(andrewheard): Configs temporarily disabled due to backend issue:
5555
// developerV1Beta,
56-
developerV1BetaStaging,
56+
// developerV1BetaStaging,
5757
developerV1Spark,
5858
developerV1BetaSpark,
5959
]
@@ -63,9 +63,9 @@ struct InstanceConfig {
6363
vertexV1Staging,
6464
vertexV1Beta,
6565
vertexV1BetaStaging,
66-
// TODO(andrewheard): Prod config temporarily disabled due to backend issue:
66+
// TODO(andrewheard): Configs temporarily disabled due to backend issue:
6767
// developerV1Beta,
68-
developerV1BetaStaging,
68+
// developerV1BetaStaging,
6969
developerV1BetaSpark,
7070
]
7171

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import FirebaseVertexAI
16+
import XCTest
17+
18+
@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
19+
final class ModalityTokenCountTests: XCTestCase {
20+
let decoder = JSONDecoder()
21+
22+
// MARK: - Decoding Tests
23+
24+
func testDecodeModalityTokenCount_valid() throws {
25+
let json = """
26+
{
27+
"modality": "TEXT",
28+
"tokenCount": 123
29+
}
30+
"""
31+
let jsonData = try XCTUnwrap(json.data(using: .utf8))
32+
33+
let tokenCount = try decoder.decode(ModalityTokenCount.self, from: jsonData)
34+
35+
XCTAssertEqual(tokenCount.modality, .text)
36+
XCTAssertEqual(tokenCount.modality.rawValue, "TEXT")
37+
XCTAssertEqual(tokenCount.tokenCount, 123)
38+
}
39+
40+
func testDecodeModalityTokenCount_missingTokenCount_defaultsToZero() throws {
41+
let json = """
42+
{
43+
"modality": "AUDIO"
44+
}
45+
"""
46+
let jsonData = try XCTUnwrap(json.data(using: .utf8))
47+
48+
let tokenCount = try decoder.decode(ModalityTokenCount.self, from: jsonData)
49+
50+
XCTAssertEqual(tokenCount.modality, .audio)
51+
XCTAssertEqual(tokenCount.modality.rawValue, "AUDIO")
52+
XCTAssertEqual(tokenCount.tokenCount, 0)
53+
}
54+
55+
func testDecodeModalityTokenCount_unrecognizedModalityString_succeeds() throws {
56+
let newModality = "NEW_MODALITY_NAME"
57+
let json = """
58+
{
59+
"modality": "\(newModality)",
60+
"tokenCount": 50
61+
}
62+
"""
63+
let jsonData = try XCTUnwrap(json.data(using: .utf8))
64+
65+
let tokenCount = try decoder.decode(ModalityTokenCount.self, from: jsonData)
66+
67+
XCTAssertEqual(tokenCount.tokenCount, 50)
68+
XCTAssertEqual(tokenCount.modality.rawValue, newModality)
69+
}
70+
71+
func testDecodeModalityTokenCount_missingModalityKey_throws() throws {
72+
let json = """
73+
{
74+
"tokenCount": 50
75+
}
76+
"""
77+
let jsonData = try XCTUnwrap(json.data(using: .utf8))
78+
79+
do {
80+
_ = try decoder.decode(ModalityTokenCount.self, from: jsonData)
81+
XCTFail("Expected a DecodingError, but decoding succeeded.")
82+
} catch let DecodingError.keyNotFound(key, _) {
83+
XCTAssertEqual(key.stringValue, "modality")
84+
} catch {
85+
XCTFail("Expected a DecodingError.keyNotFound, but received \(error)")
86+
}
87+
}
88+
}

0 commit comments

Comments
 (0)