Skip to content

Commit 08f61ec

Browse files
feat(vertexai): Add support for UsageMetaData (#12787)
* Apply changes from google ai sdk 0.3.4, and GCS feature * Add responseMimeType to config * bump the version for google ai sdk * update release note and readme * Add proxy part for storage prompt * fix trailing comma * Update packages/firebase_vertexai/firebase_vertexai/lib/src/vertex_api.dart Co-authored-by: Nate Bosch <[email protected]> * address review comments * Add support for UsageMetaData * Add dependency update * update the package for UsageMetaData * UsageMetaData first merge * merge after the refactor * better track version for dashboard * fix the documentation * remove unused function * more analyzer fix --------- Co-authored-by: Nate Bosch <[email protected]>
1 parent 35ad8d4 commit 08f61ec

File tree

4 files changed

+70
-132
lines changed

4 files changed

+70
-132
lines changed

packages/firebase_vertexai/firebase_vertexai/CHANGELOG.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,3 @@
55
## 0.1.0
66

77
- Initial release of the Vertex AI for Firebase SDK (public preview). Learn how to [get started](https://firebase.google.com/docs/vertex-ai/get-started) with the SDK in your app.
8-

packages/firebase_vertexai/firebase_vertexai/example/android/app/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
3030
android {
3131
namespace "com.example.example"
3232

33-
compileSdk 33
33+
compileSdk 34
3434

3535
defaultConfig {
3636
applicationId "com.example.example"

packages/firebase_vertexai/firebase_vertexai/lib/src/vertex_api.dart

Lines changed: 65 additions & 127 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
import 'package:google_generative_ai/google_generative_ai.dart' as google_ai;
1616
// ignore: implementation_imports, tightly coupled packages
17-
import 'package:google_generative_ai/src/vertex_hooks.dart';
17+
import 'package:google_generative_ai/src/vertex_hooks.dart' as google_ai_hooks;
1818

1919
import 'vertex_content.dart';
2020

@@ -47,32 +47,36 @@ extension GoogleAICountTokensResponseConversion
4747
/// Extension on [google_ai.CountTokensResponse] to access extra fields
4848
extension CountTokensResponseFields on google_ai.CountTokensResponse {
4949
/// Total billable Characters for the prompt.
50-
int? get totalBillableCharacters =>
51-
countTokensResponseFields(this)?['totalBillableCharacters'] as int?;
50+
int? get totalBillableCharacters => google_ai_hooks
51+
.countTokensResponseFields(this)?['totalBillableCharacters'] as int?;
5252
}
5353

5454
/// Response from the model; supports multiple candidates.
5555
final class GenerateContentResponse {
5656
/// Constructor
57-
GenerateContentResponse(this.candidates, this.promptFeedback);
57+
GenerateContentResponse(this.candidates, this.promptFeedback,
58+
{this.usageMetadata});
5859

5960
/// Candidate responses from the model.
6061
final List<Candidate> candidates;
6162

6263
/// Returns the prompt's feedback related to the content filters.
6364
final PromptFeedback? promptFeedback;
6465

66+
/// Meta data for the response
67+
final UsageMetadata? usageMetadata;
68+
6569
/// The text content of the first part of the first of [candidates], if any.
6670
///
6771
/// If the prompt was blocked, or the first candidate was finished for a reason
6872
/// of [FinishReason.recitation] or [FinishReason.safety], accessing this text
69-
/// will throw a [GenerativeAIException].
73+
/// will throw a [google_ai.GenerativeAIException].
7074
///
71-
/// If the first candidate's content starts with a text part, this value is
72-
/// that text.
75+
/// If the first candidate's content contains any text parts, this value is
76+
/// the concatenation of the text.
7377
///
74-
/// If there are no candidates, or if the first candidate does not start with
75-
/// a text part, this value is `null`.
78+
/// If there are no candidates, or if the first candidate does not contain any
79+
/// text parts, this value is `null`.
7680
String? get text {
7781
return switch (candidates) {
7882
[] => switch (promptFeedback) {
@@ -101,8 +105,12 @@ final class GenerateContentResponse {
101105
? ': $finishMessage'
102106
: ''),
103107
),
108+
// Special case for a single TextPart to avoid iterable chain.
104109
[Candidate(content: Content(parts: [TextPart(:final text)])), ...] =>
105110
text,
111+
[Candidate(content: Content(:final parts)), ...]
112+
when parts.any((p) => p is TextPart) =>
113+
parts.whereType<TextPart>().map((p) => p.text).join(),
106114
[Candidate(), ...] => null,
107115
};
108116
}
@@ -124,6 +132,7 @@ extension GoogleAIGenerateContentResponseConversion
124132
GenerateContentResponse toVertex() => GenerateContentResponse(
125133
candidates.map((c) => c.toVertex()).toList(),
126134
promptFeedback?.toVertex(),
135+
usageMetadata: usageMetadata?.toVertex(),
127136
);
128137
}
129138

@@ -239,6 +248,35 @@ extension GoogleAIPromptFeedback on google_ai.PromptFeedback {
239248
);
240249
}
241250

251+
/// Metadata on the generation request's token usage.
252+
final class UsageMetadata {
253+
/// Constructor
254+
UsageMetadata({
255+
this.promptTokenCount,
256+
this.candidatesTokenCount,
257+
this.totalTokenCount,
258+
});
259+
260+
/// Number of tokens in the prompt.
261+
final int? promptTokenCount;
262+
263+
/// Total number of tokens across the generated candidates.
264+
final int? candidatesTokenCount;
265+
266+
/// Total token count for the generation request (prompt + candidates).
267+
final int? totalTokenCount;
268+
}
269+
270+
/// Conversion utilities for [google_ai.UsageMetadata].
271+
extension GoogleAIUsageMetadata on google_ai.UsageMetadata {
272+
/// Returns this as a [UsageMetadata].
273+
UsageMetadata toVertex() => UsageMetadata(
274+
promptTokenCount: promptTokenCount,
275+
candidatesTokenCount: candidatesTokenCount,
276+
totalTokenCount: totalTokenCount,
277+
);
278+
}
279+
242280
/// Response candidate generated from a [GenerativeModel].
243281
final class Candidate {
244282
// TODO: token count?
@@ -323,6 +361,7 @@ enum BlockReason {
323361
other('OTHER');
324362

325363
const BlockReason(this._jsonString);
364+
// ignore: unused_element
326365
static BlockReason _parseValue(String jsonObject) {
327366
return switch (jsonObject) {
328367
'BLOCK_REASON_UNSPECIFIED' => BlockReason.unspecified,
@@ -374,6 +413,7 @@ enum HarmCategory {
374413
dangerousContent('HARM_CATEGORY_DANGEROUS_CONTENT');
375414

376415
const HarmCategory(this._jsonString);
416+
// ignore: unused_element
377417
static HarmCategory _parseValue(Object jsonObject) {
378418
return switch (jsonObject) {
379419
'HARM_CATEGORY_UNSPECIFIED' => HarmCategory.unspecified,
@@ -444,6 +484,7 @@ enum HarmProbability {
444484

445485
const HarmProbability(this._jsonString);
446486

487+
// ignore: unused_element
447488
static HarmProbability _parseValue(Object jsonObject) {
448489
return switch (jsonObject) {
449490
'UNSPECIFIED' => HarmProbability.unspecified,
@@ -551,6 +592,7 @@ enum FinishReason {
551592
/// Convert to json format
552593
String toJson() => _jsonString;
553594

595+
// ignore: unused_element
554596
static FinishReason _parseValue(Object jsonObject) {
555597
return switch (jsonObject) {
556598
'UNSPECIFIED' => FinishReason.unspecified,
@@ -867,132 +909,28 @@ extension TaskTypeConversion on TaskType {
867909

868910
/// Parse to [GenerateContentResponse] from json object.
869911
GenerateContentResponse parseGenerateContentResponse(Object jsonObject) {
870-
return switch (jsonObject) {
871-
{'candidates': final List<Object?> candidates} => GenerateContentResponse(
872-
candidates.map(_parseCandidate).toList(),
873-
switch (jsonObject) {
874-
{'promptFeedback': final promptFeedback?} =>
875-
_parsePromptFeedback(promptFeedback),
876-
_ => null
877-
}),
878-
{'promptFeedback': final promptFeedback?} =>
879-
GenerateContentResponse([], _parsePromptFeedback(promptFeedback)),
880-
_ => throw FormatException(
881-
'Unhandled GenerateContentResponse format', jsonObject)
882-
};
912+
google_ai.GenerateContentResponse response =
913+
google_ai_hooks.parseGenerateContentResponse(jsonObject);
914+
return response.toVertex();
883915
}
884916

885917
/// Parse to [CountTokensResponse] from json object.
886918
CountTokensResponse parseCountTokensResponse(Object jsonObject) {
887-
return switch (jsonObject) {
888-
{'totalTokens': final int totalTokens} => CountTokensResponse(totalTokens),
889-
_ =>
890-
throw FormatException('Unhandled CountTokensResponse format', jsonObject)
891-
};
919+
google_ai.CountTokensResponse response =
920+
google_ai_hooks.parseCountTokensResponse(jsonObject);
921+
return response.toVertex();
892922
}
893923

894924
/// Parse to [EmbedContentResponse] from json object.
895925
EmbedContentResponse parseEmbedContentResponse(Object jsonObject) {
896-
return switch (jsonObject) {
897-
{'embedding': final Object embedding} =>
898-
EmbedContentResponse(_parseContentEmbedding(embedding)),
899-
_ =>
900-
throw FormatException('Unhandled EmbedContentResponse format', jsonObject)
901-
};
926+
google_ai.EmbedContentResponse response =
927+
google_ai_hooks.parseEmbedContentResponse(jsonObject);
928+
return response.toVertex();
902929
}
903930

904-
Candidate _parseCandidate(Object? jsonObject) {
905-
if (jsonObject is! Map) {
906-
throw FormatException('Unhandled Candidate format', jsonObject);
907-
}
908-
909-
return Candidate(
910-
jsonObject.containsKey('content')
911-
? parseContent(jsonObject['content'] as Object)
912-
: Content(null, []),
913-
switch (jsonObject) {
914-
{'safetyRatings': final List<Object?> safetyRatings} =>
915-
safetyRatings.map(_parseSafetyRating).toList(),
916-
_ => null
917-
},
918-
switch (jsonObject) {
919-
{'citationMetadata': final Object citationMetadata} =>
920-
_parseCitationMetadata(citationMetadata),
921-
_ => null
922-
},
923-
switch (jsonObject) {
924-
{'finishReason': final Object finishReason} =>
925-
FinishReason._parseValue(finishReason),
926-
_ => null
927-
},
928-
switch (jsonObject) {
929-
{'finishMessage': final String finishMessage} => finishMessage,
930-
_ => null
931-
},
932-
);
933-
}
934-
935-
PromptFeedback _parsePromptFeedback(Object jsonObject) {
936-
return switch (jsonObject) {
937-
{
938-
'safetyRatings': final List<Object?> safetyRatings,
939-
} =>
940-
PromptFeedback(
941-
switch (jsonObject) {
942-
{'blockReason': final String blockReason} =>
943-
BlockReason._parseValue(blockReason),
944-
_ => null,
945-
},
946-
switch (jsonObject) {
947-
{'blockReasonMessage': final String blockReasonMessage} =>
948-
blockReasonMessage,
949-
_ => null,
950-
},
951-
safetyRatings.map(_parseSafetyRating).toList()),
952-
_ => throw FormatException('Unhandled PromptFeedback format', jsonObject),
953-
};
954-
}
955-
956-
SafetyRating _parseSafetyRating(Object? jsonObject) {
957-
return switch (jsonObject) {
958-
{
959-
'category': final Object category,
960-
'probability': final Object probability
961-
} =>
962-
SafetyRating(HarmCategory._parseValue(category),
963-
HarmProbability._parseValue(probability)),
964-
_ => throw FormatException('Unhandled SafetyRating format', jsonObject),
965-
};
966-
}
967-
968-
ContentEmbedding _parseContentEmbedding(Object? jsonObject) {
969-
return switch (jsonObject) {
970-
{'values': final List<Object?> values} => ContentEmbedding(<double>[
971-
...values.cast<double>(),
972-
]),
973-
_ => throw FormatException('Unhandled ContentEmbedding format', jsonObject),
974-
};
975-
}
976-
977-
CitationMetadata _parseCitationMetadata(Object? jsonObject) {
978-
return switch (jsonObject) {
979-
{'citationSources': final List<Object?> citationSources} =>
980-
CitationMetadata(citationSources.map(_parseCitationSource).toList()),
981-
_ => throw FormatException('Unhandled CitationMetadata format', jsonObject),
982-
};
983-
}
984-
985-
CitationSource _parseCitationSource(Object? jsonObject) {
986-
if (jsonObject is! Map) {
987-
throw FormatException('Unhandled CitationSource format', jsonObject);
988-
}
989-
990-
final uriString = jsonObject['uri'] as String?;
991-
992-
return CitationSource(
993-
jsonObject['startIndex'] as int?,
994-
jsonObject['endIndex'] as int?,
995-
uriString != null ? Uri.parse(uriString) : null,
996-
jsonObject['license'] as String?,
997-
);
931+
/// Parse to [BatchEmbedContentsResponse] from json object.
932+
BatchEmbedContentsResponse parseBatchEmbedContentsResponse(Object jsonObject) {
933+
google_ai.BatchEmbedContentsResponse response =
934+
google_ai_hooks.parseBatchEmbedContentsResponse(jsonObject);
935+
return response.toVertex();
998936
}

packages/firebase_vertexai/firebase_vertexai/lib/src/vertex_model.dart

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import 'package:firebase_auth/firebase_auth.dart';
2121
import 'package:firebase_core/firebase_core.dart';
2222
import 'package:google_generative_ai/google_generative_ai.dart' as google_ai;
2323
// ignore: implementation_imports, tightly coupled packages
24-
import 'package:google_generative_ai/src/vertex_hooks.dart';
24+
import 'package:google_generative_ai/src/vertex_hooks.dart' as google_ai_hooks;
2525

2626
import 'vertex_api.dart';
2727
import 'vertex_content.dart';
@@ -59,7 +59,7 @@ final class GenerativeModel {
5959
List<Tool>? tools,
6060
Content? systemInstruction,
6161
ToolConfig? toolConfig,
62-
}) : _googleAIModel = createModelWithBaseUri(
62+
}) : _googleAIModel = google_ai_hooks.createModelWithBaseUri(
6363
model: _normalizeModelName(model),
6464
apiKey: app.options.apiKey,
6565
baseUri: _vertexUri(app, location),
@@ -95,7 +95,8 @@ final class GenerativeModel {
9595
return () async {
9696
Map<String, String> headers = {};
9797
// Override the client name in Google AI SDK
98-
headers['x-goog-api-client'] = 'gl-dart/flutter fire/$packageVersion';
98+
headers['x-goog-api-client'] =
99+
'gl-dart/$packageVersion fire/$packageVersion';
99100
if (appCheck != null) {
100101
final appCheckToken = await appCheck.getToken();
101102
if (appCheckToken != null) {

0 commit comments

Comments
 (0)