From e985e0d1cc9556a6628d11a2b2599795b23f74c4 Mon Sep 17 00:00:00 2001 From: Guillaume Bernos Date: Mon, 8 Jun 2026 14:10:39 +0200 Subject: [PATCH] fix(ai): firebase_ai server template image inputs passed as InlineDataPart were serialized as normal generative inlineData --- .../firebase_ai/lib/src/base_model.dart | 27 ++++++++++++++++-- .../test/server_template_test.dart | 28 +++++++++++++++++++ 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/packages/firebase_ai/firebase_ai/lib/src/base_model.dart b/packages/firebase_ai/firebase_ai/lib/src/base_model.dart index f02bc723b580..96972eb5dafb 100644 --- a/packages/firebase_ai/firebase_ai/lib/src/base_model.dart +++ b/packages/firebase_ai/firebase_ai/lib/src/base_model.dart @@ -13,6 +13,7 @@ // limitations under the License. import 'dart:async'; +import 'dart:convert'; import 'package:firebase_app_check/firebase_app_check.dart'; import 'package:firebase_auth/firebase_auth.dart'; @@ -375,7 +376,7 @@ abstract class BaseTemplateApiClientModel extends BaseApiClientModel { T Function(Map) parse) { Map body = {}; if (inputs != null) { - body['inputs'] = inputs; + body['inputs'] = _serializeTemplateInputs(inputs); } if (history != null) { body['history'] = history.map((c) => c.toJson()).toList(); @@ -405,7 +406,7 @@ abstract class BaseTemplateApiClientModel extends BaseApiClientModel { T Function(Map) parse) { Map body = {}; if (inputs != null) { - body['inputs'] = inputs; + body['inputs'] = _serializeTemplateInputs(inputs); } if (history != null) { body['history'] = history.map((c) => c.toJson()).toList(); @@ -428,4 +429,26 @@ abstract class BaseTemplateApiClientModel extends BaseApiClientModel { /// Returns the template name for the given [templateId]. String templateName(String templateId) => _templateUri.templateName(templateId); + + Map _serializeTemplateInputs(Map inputs) { + return inputs.map((key, value) { + return MapEntry(key, _serializeTemplateInputValue(value)); + }); + } + + Object? _serializeTemplateInputValue(Object? value) { + return switch (value) { + InlineDataPart(:final mimeType, :final bytes) => { + 'isInline': true, + 'mimeType': mimeType, + 'contents': base64Encode(bytes), + }, + Map() => value.map((key, nestedValue) { + return MapEntry(key, _serializeTemplateInputValue(nestedValue)); + }), + List() => + value.map(_serializeTemplateInputValue).toList(growable: false), + _ => value, + }; + } } diff --git a/packages/firebase_ai/firebase_ai/test/server_template_test.dart b/packages/firebase_ai/firebase_ai/test/server_template_test.dart index 71d0905a7424..1dbde35afc0c 100644 --- a/packages/firebase_ai/firebase_ai/test/server_template_test.dart +++ b/packages/firebase_ai/firebase_ai/test/server_template_test.dart @@ -13,6 +13,7 @@ // limitations under the License. import 'dart:convert'; +import 'dart:typed_data'; import 'package:firebase_ai/firebase_ai.dart'; import 'package:firebase_ai/src/base_model.dart'; @@ -86,6 +87,33 @@ void main() { expect(response.text, 'Some response'); }); + test('generateContent serializes inline image inputs', () async { + final mockHttp = MockClient((request) async { + final body = jsonDecode(request.body) as Map; + expect(body['inputs'], { + 'screenshot': { + 'isInline': true, + 'mimeType': 'image/jpeg', + 'contents': base64Encode([1, 2, 3]), + }, + }); + return http.Response(jsonEncode(_arbitraryGenerateContentResponse), 200, + headers: {'content-type': 'application/json'}); + }); + + final model = createModel(mockHttp); + final response = await model.generateContent( + templateId, + inputs: { + 'screenshot': InlineDataPart( + 'image/jpeg', + Uint8List.fromList([1, 2, 3]), + ), + }, + ); + expect(response.text, 'Some response'); + }); + test('generateContent with TemplateToolConfig passes retrievalConfig', () async { final mockHttp = MockClient((request) async {