Skip to content

Commit 3879094

Browse files
andrewheardcherylEnkidu
authored andcommitted
[Firebase AI] Deprecate CountTokensResponse.totalBillableCharacters (#14998)
1 parent fa3529a commit 3879094

File tree

5 files changed

+39
-19
lines changed

5 files changed

+39
-19
lines changed

FirebaseAI/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
types. (#14971)
55
- [added] Added support for configuring the "thinking" budget when using Gemini
66
2.5 series models. (#14909)
7+
- [changed] Deprecated `CountTokensResponse.totalBillableCharacters`; use
8+
`totalTokens` instead. Gemini 2.0 series models and newer are always billed by
9+
token count. (#14934)
710

811
# 11.13.0
912
- [feature] Initial release of the Firebase AI Logic SDK (`FirebaseAI`). This

FirebaseAI/Sources/Types/Internal/Requests/CountTokensRequest.swift

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,17 +39,31 @@ extension CountTokensRequest: GenerativeAIRequest {
3939
/// The model's response to a count tokens request.
4040
@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
4141
public struct CountTokensResponse: Sendable {
42+
/// Container for deprecated properties or methods.
43+
///
44+
/// This workaround allows deprecated fields to be referenced internally (for example in the
45+
/// `init(from:)` constructor) without introducing compiler warnings.
46+
struct Deprecated {
47+
let totalBillableCharacters: Int?
48+
}
49+
4250
/// The total number of tokens in the input given to the model as a prompt.
4351
public let totalTokens: Int
4452

4553
/// The total number of billable characters in the text input given to the model as a prompt.
4654
///
4755
/// > Important: This does not include billable image, video or other non-text input. See
4856
/// [Vertex AI pricing](https://firebase.google.com/docs/vertex-ai/pricing) for details.
49-
public let totalBillableCharacters: Int?
57+
@available(*, deprecated, message: """
58+
Use `totalTokens` instead; Gemini 2.0 series models and newer are always billed by token count.
59+
""")
60+
public var totalBillableCharacters: Int? { deprecated.totalBillableCharacters }
5061

5162
/// The breakdown, by modality, of how many tokens are consumed by the prompt.
5263
public let promptTokensDetails: [ModalityTokenCount]
64+
65+
/// Deprecated properties or methods.
66+
let deprecated: Deprecated
5367
}
5468

5569
// MARK: - Codable Conformances
@@ -105,9 +119,10 @@ extension CountTokensResponse: Decodable {
105119
public init(from decoder: any Decoder) throws {
106120
let container = try decoder.container(keyedBy: CodingKeys.self)
107121
totalTokens = try container.decodeIfPresent(Int.self, forKey: .totalTokens) ?? 0
108-
totalBillableCharacters =
109-
try container.decodeIfPresent(Int.self, forKey: .totalBillableCharacters)
110122
promptTokensDetails =
111123
try container.decodeIfPresent([ModalityTokenCount].self, forKey: .promptTokensDetails) ?? []
124+
let totalBillableCharacters =
125+
try container.decodeIfPresent(Int.self, forKey: .totalBillableCharacters)
126+
deprecated = CountTokensResponse.Deprecated(totalBillableCharacters: totalBillableCharacters)
112127
}
113128
}

FirebaseAI/Tests/TestApp/Tests/Integration/CountTokensIntegrationTests.swift

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,9 @@ struct CountTokensIntegrationTests {
5959
#expect(response.totalTokens == 6)
6060
switch config.apiConfig.service {
6161
case .vertexAI:
62-
#expect(response.totalBillableCharacters == 16)
62+
#expect(response.deprecated.totalBillableCharacters == 16)
6363
case .googleAI:
64-
#expect(response.totalBillableCharacters == nil)
64+
#expect(response.deprecated.totalBillableCharacters == nil)
6565
}
6666
#expect(response.promptTokensDetails.count == 1)
6767
let promptTokensDetails = try #require(response.promptTokensDetails.first)
@@ -83,9 +83,9 @@ struct CountTokensIntegrationTests {
8383
#expect(response.totalTokens == 14)
8484
switch config.apiConfig.service {
8585
case .vertexAI:
86-
#expect(response.totalBillableCharacters == 61)
86+
#expect(response.deprecated.totalBillableCharacters == 61)
8787
case .googleAI:
88-
#expect(response.totalBillableCharacters == nil)
88+
#expect(response.deprecated.totalBillableCharacters == nil)
8989
}
9090
#expect(response.promptTokensDetails.count == 1)
9191
let promptTokensDetails = try #require(response.promptTokensDetails.first)
@@ -115,12 +115,12 @@ struct CountTokensIntegrationTests {
115115
switch config.apiConfig.service {
116116
case .vertexAI:
117117
#expect(response.totalTokens == 65)
118-
#expect(response.totalBillableCharacters == 170)
118+
#expect(response.deprecated.totalBillableCharacters == 170)
119119
case .googleAI:
120120
// The Developer API erroneously ignores the `responseSchema` when counting tokens, resulting
121121
// in a lower total count than Vertex AI.
122122
#expect(response.totalTokens == 34)
123-
#expect(response.totalBillableCharacters == nil)
123+
#expect(response.deprecated.totalBillableCharacters == nil)
124124
}
125125
#expect(response.promptTokensDetails.count == 1)
126126
let promptTokensDetails = try #require(response.promptTokensDetails.first)

FirebaseAI/Tests/TestApp/Tests/Integration/IntegrationTests.swift

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import FirebaseCore
1919
import FirebaseStorage
2020
import XCTest
2121

22+
@testable import struct FirebaseAI.CountTokensRequest
23+
2224
// TODO(#14405): Migrate to Swift Testing and parameterize tests.
2325
final class IntegrationTests: XCTestCase {
2426
// Set temperature, topP and topK to lowest allowed values to make responses more deterministic.
@@ -83,7 +85,7 @@ final class IntegrationTests: XCTestCase {
8385
let response = try await model.countTokens(prompt)
8486

8587
XCTAssertEqual(response.totalTokens, 14)
86-
XCTAssertEqual(response.totalBillableCharacters, 51)
88+
XCTAssertEqual(response.deprecated.totalBillableCharacters, 51)
8789
XCTAssertEqual(response.promptTokensDetails.count, 1)
8890
let promptTokensDetails = try XCTUnwrap(response.promptTokensDetails.first)
8991
XCTAssertEqual(promptTokensDetails.modality, .text)
@@ -100,7 +102,7 @@ final class IntegrationTests: XCTestCase {
100102
let response = try await model.countTokens(image)
101103

102104
XCTAssertEqual(response.totalTokens, 266)
103-
XCTAssertEqual(response.totalBillableCharacters, 35)
105+
XCTAssertEqual(response.deprecated.totalBillableCharacters, 35)
104106
XCTAssertEqual(response.promptTokensDetails.count, 2) // Image prompt + system instruction
105107
let textPromptTokensDetails = try XCTUnwrap(response.promptTokensDetails.first {
106108
$0.modality == .text
@@ -120,7 +122,7 @@ final class IntegrationTests: XCTestCase {
120122
let response = try await model.countTokens(fileData)
121123

122124
XCTAssertEqual(response.totalTokens, 266)
123-
XCTAssertEqual(response.totalBillableCharacters, 35)
125+
XCTAssertEqual(response.deprecated.totalBillableCharacters, 35)
124126
XCTAssertEqual(response.promptTokensDetails.count, 2) // Image prompt + system instruction
125127
let textPromptTokensDetails = try XCTUnwrap(response.promptTokensDetails.first {
126128
$0.modality == .text
@@ -139,7 +141,7 @@ final class IntegrationTests: XCTestCase {
139141
let response = try await model.countTokens(fileData)
140142

141143
XCTAssertEqual(response.totalTokens, 266)
142-
XCTAssertEqual(response.totalBillableCharacters, 35)
144+
XCTAssertEqual(response.deprecated.totalBillableCharacters, 35)
143145
}
144146

145147
func testCountTokens_image_fileData_requiresUserAuth_userSignedIn() async throws {
@@ -150,7 +152,7 @@ final class IntegrationTests: XCTestCase {
150152
let response = try await model.countTokens(fileData)
151153

152154
XCTAssertEqual(response.totalTokens, 266)
153-
XCTAssertEqual(response.totalBillableCharacters, 35)
155+
XCTAssertEqual(response.deprecated.totalBillableCharacters, 35)
154156
}
155157

156158
func testCountTokens_image_fileData_requiresUserAuth_wrongUser_permissionDenied() async throws {
@@ -191,7 +193,7 @@ final class IntegrationTests: XCTestCase {
191193
])
192194

193195
XCTAssertGreaterThan(response.totalTokens, 0)
194-
XCTAssertEqual(response.totalBillableCharacters, 71)
196+
XCTAssertEqual(response.deprecated.totalBillableCharacters, 71)
195197
XCTAssertEqual(response.promptTokensDetails.count, 1)
196198
let promptTokensDetails = try XCTUnwrap(response.promptTokensDetails.first)
197199
XCTAssertEqual(promptTokensDetails.modality, .text)

FirebaseAI/Tests/Unit/GenerativeModelVertexAITests.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1517,7 +1517,7 @@ final class GenerativeModelVertexAITests: XCTestCase {
15171517
let response = try await model.countTokens("Why is the sky blue?")
15181518

15191519
XCTAssertEqual(response.totalTokens, 6)
1520-
XCTAssertEqual(response.totalBillableCharacters, 16)
1520+
XCTAssertEqual(response.deprecated.totalBillableCharacters, 16)
15211521
}
15221522

15231523
func testCountTokens_succeeds_detailed() async throws {
@@ -1530,7 +1530,7 @@ final class GenerativeModelVertexAITests: XCTestCase {
15301530
let response = try await model.countTokens("Why is the sky blue?")
15311531

15321532
XCTAssertEqual(response.totalTokens, 1837)
1533-
XCTAssertEqual(response.totalBillableCharacters, 117)
1533+
XCTAssertEqual(response.deprecated.totalBillableCharacters, 117)
15341534
XCTAssertEqual(response.promptTokensDetails.count, 2)
15351535
XCTAssertEqual(response.promptTokensDetails[0].modality, .image)
15361536
XCTAssertEqual(response.promptTokensDetails[0].tokenCount, 1806)
@@ -1577,7 +1577,7 @@ final class GenerativeModelVertexAITests: XCTestCase {
15771577
let response = try await model.countTokens("Why is the sky blue?")
15781578

15791579
XCTAssertEqual(response.totalTokens, 6)
1580-
XCTAssertEqual(response.totalBillableCharacters, 16)
1580+
XCTAssertEqual(response.deprecated.totalBillableCharacters, 16)
15811581
}
15821582

15831583
func testCountTokens_succeeds_noBillableCharacters() async throws {
@@ -1590,7 +1590,7 @@ final class GenerativeModelVertexAITests: XCTestCase {
15901590
let response = try await model.countTokens(InlineDataPart(data: Data(), mimeType: "image/jpeg"))
15911591

15921592
XCTAssertEqual(response.totalTokens, 258)
1593-
XCTAssertNil(response.totalBillableCharacters)
1593+
XCTAssertNil(response.deprecated.totalBillableCharacters)
15941594
}
15951595

15961596
func testCountTokens_modelNotFound() async throws {

0 commit comments

Comments
 (0)