Skip to content

Commit e06b4ce

Browse files
authored
[VertexAI] Add initial support for BiDi streaming (#1230)
* [VertexAI] Add initial support for BiDi streaming * Resolve some of the TODOs * Some cleanup
1 parent 62a6d4d commit e06b4ce

15 files changed

+925
-7
lines changed

vertexai/src/GenerativeModel.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ public class GenerativeModel {
4949

5050
/// <summary>
5151
/// Intended for internal use only.
52-
/// Use `VertexAI.GetInstance` instead to ensure proper initialization and configuration of the `GenerativeModel`.
52+
/// Use `VertexAI.GetGenerativeModel` instead to ensure proper initialization and configuration of the `GenerativeModel`.
5353
/// </summary>
5454
internal GenerativeModel(FirebaseApp firebaseApp,
5555
string location,
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
* Copyright 2025 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
namespace Firebase.VertexAI.Internal {
18+
19+
// Contains extension methods for converting shared enums to strings.
20+
internal static class EnumConverters {
21+
22+
public static string ResponseModalityToString(this ResponseModality modality) {
23+
return modality switch {
24+
ResponseModality.Text => "TEXT",
25+
ResponseModality.Image => "IMAGE",
26+
ResponseModality.Audio => "AUDIO",
27+
_ => throw new System.ArgumentOutOfRangeException(nameof(modality), "Unsupported Modality type")
28+
};
29+
}
30+
}
31+
32+
}

vertexai/src/Internal/EnumConverters.cs.meta

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

vertexai/src/LiveContentResponse.cs

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
/*
2+
* Copyright 2025 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
using System.Collections.Generic;
18+
using System.Collections.ObjectModel;
19+
using Google.MiniJSON;
20+
using Firebase.VertexAI.Internal;
21+
using System.Linq;
22+
using System;
23+
24+
namespace Firebase.VertexAI {
25+
26+
/// <summary>
27+
/// Represents the response from the model for live content updates.
28+
/// </summary>
29+
public readonly struct LiveContentResponse {
30+
/// <summary>
31+
/// Represents the status of the `LiveContentResponse`.
32+
/// </summary>
33+
public enum LiveResponseStatus {
34+
/// <summary>
35+
/// Indicates response has no special status associated with it.
36+
/// </summary>
37+
Normal,
38+
/// <summary>
39+
/// Indicates the model is done generating content.
40+
/// </summary>
41+
TurnComplete,
42+
/// <summary>
43+
/// Indicates a client message has interrupted the current generation.
44+
/// </summary>
45+
Interrupted
46+
}
47+
48+
/// <summary>
49+
/// The main content data of the response. This can be `null` if there is no content.
50+
/// </summary>
51+
public ModelContent? Content { get; }
52+
53+
/// <summary>
54+
/// The status of the response.
55+
/// </summary>
56+
public LiveResponseStatus Status { get; }
57+
58+
private readonly ReadOnlyCollection<ModelContent.FunctionCallPart> _functionCalls;
59+
60+
/// <summary>
61+
/// A list of [FunctionCallPart] included in the response, if any.
62+
///
63+
/// This will be empty if no function calls are present.
64+
/// </summary>
65+
public IEnumerable<ModelContent.FunctionCallPart> FunctionCalls =>
66+
_functionCalls ?? new ReadOnlyCollection<ModelContent.FunctionCallPart>(
67+
new List<ModelContent.FunctionCallPart>());
68+
69+
// TODO: Add this
70+
//public IEnumerable<string> FunctionIdsToCancel { get; }
71+
72+
/// <summary>
73+
/// The response's content as text, if it exists.
74+
/// </summary>
75+
public string Text {
76+
get {
77+
string str = "";
78+
if (Content != null) {
79+
foreach (var part in Content?.Parts) {
80+
if (part is ModelContent.TextPart textPart) {
81+
str += textPart.Text;
82+
}
83+
}
84+
}
85+
return str;
86+
}
87+
}
88+
89+
/// <summary>
90+
/// The response's content that was audio, if it exists.
91+
/// </summary>
92+
public IEnumerable<byte[]> Audio {
93+
get {
94+
return Content?.Parts
95+
.OfType<ModelContent.InlineDataPart>()
96+
.Where(part => part.MimeType == "audio/pcm")
97+
.Select(part => part.Data.ToArray());
98+
}
99+
}
100+
101+
/// <summary>
102+
/// The response's content that was audio, if it exists, converted into floats.
103+
/// </summary>
104+
public IEnumerable<float[]> AudioAsFloat {
105+
get {
106+
return Audio?.Select(ConvertBytesToFloat);
107+
}
108+
}
109+
110+
private float[] ConvertBytesToFloat(byte[] byteArray) {
111+
// Assumes 16 bit encoding, which would be two bytes per sample.
112+
int sampleCount = byteArray.Length / 2;
113+
float[] floatArray = new float[sampleCount];
114+
115+
for (int i = 0; i < sampleCount; i++) {
116+
float sample = (short)(byteArray[i * 2] | (byteArray[i * 2 + 1] << 8)) / 32768f;
117+
floatArray[i] = Math.Clamp(sample, -1f, 1f); // Ensure values are within the valid range
118+
}
119+
120+
return floatArray;
121+
}
122+
123+
private LiveContentResponse(ModelContent? content, LiveResponseStatus status) {
124+
Content = content;
125+
Status = status;
126+
_functionCalls = new ReadOnlyCollection<ModelContent.FunctionCallPart>(
127+
new List<ModelContent.FunctionCallPart>());
128+
}
129+
130+
private LiveContentResponse(List<ModelContent.FunctionCallPart> functionCalls) {
131+
Content = null;
132+
Status = LiveResponseStatus.Normal;
133+
_functionCalls = new ReadOnlyCollection<ModelContent.FunctionCallPart>(
134+
functionCalls ?? new List<ModelContent.FunctionCallPart>());
135+
}
136+
137+
/// <summary>
138+
/// Intended for internal use only.
139+
/// This method is used for deserializing JSON responses and should not be called directly.
140+
/// </summary>
141+
internal static LiveContentResponse? FromJson(string jsonString) {
142+
return FromJson(Json.Deserialize(jsonString) as Dictionary<string, object>);
143+
}
144+
145+
/// <summary>
146+
/// Intended for internal use only.
147+
/// This method is used for deserializing JSON responses and should not be called directly.
148+
/// </summary>
149+
internal static LiveContentResponse? FromJson(Dictionary<string, object> jsonDict) {
150+
if (jsonDict.ContainsKey("setupComplete")) {
151+
// We don't want to pass this along to the user, so return null instead.
152+
return null;
153+
} else if (jsonDict.TryParseValue("serverContent", out Dictionary<string, object> serverContent)) {
154+
bool turnComplete = serverContent.ParseValue<bool>("turnComplete");
155+
bool interrupted = serverContent.ParseValue<bool>("interrupted");
156+
LiveResponseStatus status = interrupted ? LiveResponseStatus.Interrupted :
157+
turnComplete ? LiveResponseStatus.TurnComplete : LiveResponseStatus.Normal;
158+
return new LiveContentResponse(
159+
serverContent.ParseNullableObject("modelTurn", ModelContent.FromJson),
160+
status);
161+
} else if (jsonDict.TryParseValue("toolCall", out Dictionary<string, object> toolCall)) {
162+
return new LiveContentResponse(
163+
jsonDict.ParseObjectList("functionCalls", ModelContentJsonParsers.FunctionCallPartFromJson));
164+
} else {
165+
// TODO: Determine if we want to log this, or just ignore it?
166+
#if FIREBASE_LOG_REST_CALLS
167+
UnityEngine.Debug.Log($"Failed to parse LiveContentResponse from JSON, with keys: {string.Join(',', jsonDict.Keys)}");
168+
#endif
169+
return null;
170+
}
171+
}
172+
}
173+
174+
}

vertexai/src/LiveContentResponse.cs.meta

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)