Visitar URL original
feat(ai): add support for cancellable clients for in-flight requests … · firebase/flutterfire@566cfed · GitHub
Skip to content

Commit 566cfed

Browse files
authored
feat(ai): add support for cancellable clients for in-flight requests (#18349)
1 parent b3c835f commit 566cfed

3 files changed

Lines changed: 84 additions & 0 deletions

File tree

‎packages/firebase_ai/firebase_ai/lib/src/firebase_ai.dart‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import 'package:firebase_auth/firebase_auth.dart';
1717
import 'package:firebase_core/firebase_core.dart';
1818
import 'package:firebase_core_platform_interface/firebase_core_platform_interface.dart'
1919
show FirebasePluginPlatform;
20+
import 'package:http/http.dart' as http;
2021
import 'package:meta/meta.dart';
2122

2223
import '../firebase_ai.dart';
@@ -142,13 +143,17 @@ class FirebaseAI extends FirebasePluginPlatform {
142143
/// The optional [safetySettings] and [generationConfig] can be used to
143144
/// control and guide the generation. See [SafetySetting] and
144145
/// [GenerationConfig] for details.
146+
///
147+
/// The optional [httpClient] can be used to customize HTTP request handling,
148+
/// such as adding support for cancelling in-flight requests.
145149
GenerativeModel generativeModel({
146150
required String model,
147151
List<SafetySetting>? safetySettings,
148152
GenerationConfig? generationConfig,
149153
List<Tool>? tools,
150154
ToolConfig? toolConfig,
151155
Content? systemInstruction,
156+
http.Client? httpClient,
152157
}) {
153158
return createGenerativeModel(
154159
model: model,
@@ -163,6 +168,7 @@ class FirebaseAI extends FirebasePluginPlatform {
163168
toolConfig: toolConfig,
164169
systemInstruction: systemInstruction,
165170
useLimitedUseAppCheckTokens: useLimitedUseAppCheckTokens,
171+
httpClient: httpClient,
166172
);
167173
}
168174

‎packages/firebase_ai/firebase_ai/lib/src/generative_model.dart‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,7 @@ GenerativeModel createGenerativeModel({
210210
List<Tool>? tools,
211211
ToolConfig? toolConfig,
212212
Content? systemInstruction,
213+
http.Client? httpClient,
213214
}) =>
214215
GenerativeModel._(
215216
model: model,
@@ -224,6 +225,7 @@ GenerativeModel createGenerativeModel({
224225
tools: tools,
225226
toolConfig: toolConfig,
226227
systemInstruction: systemInstruction,
228+
httpClient: httpClient,
227229
);
228230

229231
/// Creates a model with an overridden [ApiClient] for testing.

‎packages/firebase_ai/firebase_ai/test/firebase_vertexai_test.dart‎

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,15 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15+
import 'dart:convert';
16+
1517
import 'package:firebase_ai/firebase_ai.dart';
1618
import 'package:firebase_app_check/firebase_app_check.dart';
1719
import 'package:firebase_auth/firebase_auth.dart';
1820
import 'package:firebase_core/firebase_core.dart';
21+
import 'package:flutter/services.dart';
1922
import 'package:flutter_test/flutter_test.dart';
23+
import 'package:http/http.dart' as http;
2024

2125
import 'mock.dart';
2226

@@ -50,6 +54,16 @@ void main() {
5054
customAppCheck = FirebaseAppCheck.instanceFor(app: customApp);
5155
limitTokenAppCheck = FirebaseAppCheck.instanceFor(app: limitTokenApp);
5256
customAuth = FirebaseAuth.instanceFor(app: customApp);
57+
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
58+
.setMockMessageHandler(
59+
'dev.flutter.pigeon.firebase_app_check_platform_interface.'
60+
'FirebaseAppCheckHostApi.getToken',
61+
(_) async {
62+
return const StandardMessageCodec().encodeMessage(
63+
<Object?>['app-check-token'],
64+
);
65+
},
66+
);
5367
});
5468

5569
test('Singleton behavior', () {
@@ -129,5 +143,67 @@ void main() {
129143

130144
expect(model, isA<GenerativeModel>());
131145
});
146+
147+
test('generativeModel uses provided HTTP client', () async {
148+
final requests = <http.BaseRequest>[];
149+
final client = _RecordingClient((request) {
150+
requests.add(request);
151+
152+
if (request.url.path.endsWith(':streamGenerateContent')) {
153+
return http.StreamedResponse(
154+
Stream.value(utf8.encode('data: ${jsonEncode(_response)}\n\n')),
155+
200,
156+
);
157+
}
158+
159+
return http.StreamedResponse(
160+
Stream.value(utf8.encode(jsonEncode(_response))),
161+
200,
162+
);
163+
});
164+
final ai = FirebaseAI.googleAI(app: app);
165+
166+
final model = ai.generativeModel(
167+
model: 'gemini-pro',
168+
httpClient: client,
169+
);
170+
171+
await model.generateContent([Content.text('prompt')]);
172+
await model.generateContentStream([Content.text('prompt')]).drain<void>();
173+
174+
expect(requests, hasLength(2));
175+
expect(
176+
requests.first.url.path,
177+
endsWith('/models/gemini-pro:generateContent'),
178+
);
179+
expect(
180+
requests.last.url.path,
181+
endsWith('/models/gemini-pro:streamGenerateContent'),
182+
);
183+
});
132184
});
133185
}
186+
187+
class _RecordingClient extends http.BaseClient {
188+
_RecordingClient(this._handler);
189+
190+
final http.StreamedResponse Function(http.BaseRequest request) _handler;
191+
192+
@override
193+
Future<http.StreamedResponse> send(http.BaseRequest request) async {
194+
return _handler(request);
195+
}
196+
}
197+
198+
const _response = {
199+
'candidates': [
200+
{
201+
'content': {
202+
'role': 'model',
203+
'parts': [
204+
{'text': 'Some Response'},
205+
],
206+
},
207+
},
208+
],
209+
};

0 commit comments

Comments
 (0)