Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 29 additions & 17 deletions packages/firebase_ai/firebase_ai/lib/src/generative_model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -114,22 +114,27 @@ final class GenerativeModel extends BaseApiClientModel {
/// print(response.text);
/// ```
Future<GenerateContentResponse> generateContent(Iterable<Content> prompt,
{List<SafetySetting>? safetySettings,
GenerationConfig? generationConfig,
List<Tool>? tools,
ToolConfig? toolConfig}) =>
makeRequest(
Task.generateContent,
_serializationStrategy.generateContentRequest(
prompt,
model,
safetySettings ?? _safetySettings,
generationConfig ?? _generationConfig,
tools ?? this.tools,
toolConfig ?? _toolConfig,
_systemInstruction,
),
_serializationStrategy.parseGenerateContentResponse);
{List<SafetySetting>? safetySettings,
GenerationConfig? generationConfig,
List<Tool>? tools,
ToolConfig? toolConfig}) {
final resolvedTools = tools ?? this.tools;
if (resolvedTools != null) {
Tool.validateToolCombination(resolvedTools);
}
return makeRequest(
Task.generateContent,
_serializationStrategy.generateContentRequest(
prompt,
model,
safetySettings ?? _safetySettings,
generationConfig ?? _generationConfig,
resolvedTools,
toolConfig ?? _toolConfig,
_systemInstruction,
),
_serializationStrategy.parseGenerateContentResponse);
}

/// Generates a stream of content responding to [prompt].
///
Expand All @@ -149,14 +154,18 @@ final class GenerativeModel extends BaseApiClientModel {
GenerationConfig? generationConfig,
List<Tool>? tools,
ToolConfig? toolConfig}) {
final resolvedTools = tools ?? this.tools;
if (resolvedTools != null) {
Tool.validateToolCombination(resolvedTools);
}
final response = client.streamRequest(
taskUri(Task.streamGenerateContent),
_serializationStrategy.generateContentRequest(
prompt,
model,
safetySettings ?? _safetySettings,
generationConfig ?? _generationConfig,
tools ?? this.tools,
resolvedTools,
toolConfig ?? _toolConfig,
_systemInstruction,
));
Expand All @@ -183,6 +192,9 @@ final class GenerativeModel extends BaseApiClientModel {
Future<CountTokensResponse> countTokens(
Iterable<Content> contents,
) async {
if (tools != null) {
Tool.validateToolCombination(tools!);
}
final parameters = _serializationStrategy.countTokensRequest(
contents,
model,
Expand Down
37 changes: 37 additions & 0 deletions packages/firebase_ai/firebase_ai/lib/src/tool.dart
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ final class Tool {
this._urlContext);

/// Returns a [Tool] instance with list of [FunctionDeclaration].
///
/// **Note:** Function declarations cannot be combined with other tool types
/// such as [googleSearch], [codeExecution], or [urlContext] in the same
/// [tools] list. The Gemini API does not support mixing function calling
/// with other tool types in a single request. Use separate model instances
/// or separate requests for different tool types.
static Tool functionDeclarations(
List<FunctionDeclaration> functionDeclarations) {
return Tool._(functionDeclarations, null, null, null);
Expand All @@ -48,6 +54,10 @@ final class Tool {
/// - [googleSearch]: An empty [GoogleSearch] object. The presence of this
/// object in the list of tools enables the model to use Google Search.
///
/// **Note:** Google Search cannot be combined with [functionDeclarations]
/// in the same [tools] list. The Gemini API does not support mixing function
/// calling with other tool types in a single request.
///
/// Returns a `Tool` configured for Google Search.
static Tool googleSearch({GoogleSearch googleSearch = const GoogleSearch()}) {
return Tool._(null, googleSearch, null, null);
Expand Down Expand Up @@ -106,6 +116,33 @@ final class Tool {
[];
}

/// Validates that a list of [tools] does not contain incompatible
/// combinations.
///
/// The Gemini API does not support mixing [functionDeclarations] with other
/// tool types ([googleSearch], [codeExecution], [urlContext]) in the same
/// request. This method throws an [ArgumentError] if such a combination is
/// detected, providing a clear error message instead of a cryptic server
/// error.
static void validateToolCombination(List<Tool> tools) {
final hasFunctionDeclarations =
tools.any((t) => t._functionDeclarations != null);
final hasOtherTools = tools.any((t) =>
t._googleSearch != null ||
t._codeExecution != null ||
t._urlContext != null);

if (hasFunctionDeclarations && hasOtherTools) {
throw ArgumentError(
'Function declarations cannot be combined with other tool types '
'(googleSearch, codeExecution, urlContext) in the same request. '
'The Gemini API does not support mixing function calling with other '
'tool types. Use separate model instances or separate requests for '
'different tool types.',
);
}
}

/// Convert to json object.
Map<String, Object> toJson() => {
if (_functionDeclarations case final _functionDeclarations?)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,55 @@ void main() {
);
});

test(
'throws ArgumentError when mixing functionDeclarations with googleSearch',
() async {
final (_, model) = createModel(
tools: [
Tool.functionDeclarations([
FunctionDeclaration(
'someFunction',
'Some cool function.',
parameters: {
'param': Schema.string(description: 'Some parameter.'),
},
),
]),
Tool.googleSearch(),
],
);
const prompt = 'Some prompt';
expect(
() => model.generateContent([Content.text(prompt)]),
throwsArgumentError,
);
});

test(
'throws ArgumentError when mixing functionDeclarations with googleSearch via override',
() async {
final (_, model) = createModel();
const prompt = 'Some prompt';
expect(
() => model.generateContent(
[Content.text(prompt)],
tools: [
Tool.functionDeclarations([
FunctionDeclaration(
'someFunction',
'Some cool function.',
parameters: {
'param': Schema.string(description: 'Some parameter.'),
},
),
]),
Tool.googleSearch(),
],
),
throwsArgumentError,
);
});

test('can pass a url context tool', () async {
final (client, model) = createModel(
tools: [Tool.urlContext()],
Expand Down Expand Up @@ -510,6 +559,30 @@ void main() {
);
await responses.drain<void>();
});

test(
'throws ArgumentError when mixing functionDeclarations with googleSearch',
() async {
final (_, model) = createModel(
tools: [
Tool.functionDeclarations([
FunctionDeclaration(
'someFunction',
'Some cool function.',
parameters: {
'param': Schema.string(description: 'Some parameter.'),
},
),
]),
Tool.googleSearch(),
],
);
const prompt = 'Some prompt';
expect(
() => model.generateContentStream([Content.text(prompt)]),
throwsArgumentError,
);
});
});

group('count tokens', () {
Expand Down
98 changes: 98 additions & 0 deletions packages/firebase_ai/firebase_ai/test/tool_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,104 @@ void main() {
});
});

// Test Tool.validateToolCombination
group('validateToolCombination', () {
test('allows function declarations alone', () {
expect(
() => Tool.validateToolCombination([
Tool.functionDeclarations([
FunctionDeclaration('fn', 'desc',
parameters: {'p': Schema.string()}),
]),
]),
returnsNormally,
);
});

test('allows googleSearch alone', () {
expect(
() => Tool.validateToolCombination([Tool.googleSearch()]),
returnsNormally,
);
});

test('allows codeExecution alone', () {
expect(
() => Tool.validateToolCombination([Tool.codeExecution()]),
returnsNormally,
);
});

test('allows urlContext alone', () {
expect(
() => Tool.validateToolCombination([Tool.urlContext()]),
returnsNormally,
);
});

test('allows empty list', () {
expect(
() => Tool.validateToolCombination([]),
returnsNormally,
);
});

test('throws when mixing functionDeclarations with googleSearch', () {
expect(
() => Tool.validateToolCombination([
Tool.functionDeclarations([
FunctionDeclaration('fn', 'desc',
parameters: {'p': Schema.string()}),
]),
Tool.googleSearch(),
]),
throwsArgumentError,
);
});

test('throws when mixing functionDeclarations with codeExecution', () {
expect(
() => Tool.validateToolCombination([
Tool.functionDeclarations([
FunctionDeclaration('fn', 'desc',
parameters: {'p': Schema.string()}),
]),
Tool.codeExecution(),
]),
throwsArgumentError,
);
});

test('throws when mixing functionDeclarations with urlContext', () {
expect(
() => Tool.validateToolCombination([
Tool.functionDeclarations([
FunctionDeclaration('fn', 'desc',
parameters: {'p': Schema.string()}),
]),
Tool.urlContext(),
]),
throwsArgumentError,
);
});

test('allows multiple function declaration tools', () {
expect(
() => Tool.validateToolCombination([
Tool.functionDeclarations([
FunctionDeclaration('fn1', 'desc1',
parameters: {'p': Schema.string()}),
]),
Tool.functionDeclarations([
FunctionDeclaration('fn2', 'desc2',
parameters: {'p': Schema.string()}),
]),
]),
returnsNormally,
);
});
});

// Test FunctionCallingConfig
test('FunctionCallingConfig.auto()', () {
final config = FunctionCallingConfig.auto();
Expand Down
Loading