Skip to content

Commit 88b3f74

Browse files
committed
feat: enable native tool calling for gemini provider
1 parent f7c2e8d commit 88b3f74

File tree

3 files changed

+87
-8
lines changed

3 files changed

+87
-8
lines changed

packages/types/src/providers/gemini.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export const geminiModels = {
1111
maxTokens: 64_000,
1212
contextWindow: 1_048_576,
1313
supportsImages: true,
14+
supportsNativeTools: true,
1415
supportsPromptCache: true,
1516
inputPrice: 2.5, // This is the pricing for prompts above 200k tokens.
1617
outputPrice: 15,
@@ -38,6 +39,7 @@ export const geminiModels = {
3839
maxTokens: 65_535,
3940
contextWindow: 1_048_576,
4041
supportsImages: true,
42+
supportsNativeTools: true,
4143
supportsPromptCache: true,
4244
inputPrice: 2.5, // This is the pricing for prompts above 200k tokens.
4345
outputPrice: 15,
@@ -64,6 +66,7 @@ export const geminiModels = {
6466
maxTokens: 65_535,
6567
contextWindow: 1_048_576,
6668
supportsImages: true,
69+
supportsNativeTools: true,
6770
supportsPromptCache: true,
6871
inputPrice: 2.5, // This is the pricing for prompts above 200k tokens.
6972
outputPrice: 15,
@@ -88,6 +91,7 @@ export const geminiModels = {
8891
maxTokens: 65_535,
8992
contextWindow: 1_048_576,
9093
supportsImages: true,
94+
supportsNativeTools: true,
9195
supportsPromptCache: true,
9296
inputPrice: 2.5, // This is the pricing for prompts above 200k tokens.
9397
outputPrice: 15,
@@ -116,6 +120,7 @@ export const geminiModels = {
116120
maxTokens: 65_536,
117121
contextWindow: 1_048_576,
118122
supportsImages: true,
123+
supportsNativeTools: true,
119124
supportsPromptCache: true,
120125
inputPrice: 0.3,
121126
outputPrice: 2.5,
@@ -128,6 +133,7 @@ export const geminiModels = {
128133
maxTokens: 65_536,
129134
contextWindow: 1_048_576,
130135
supportsImages: true,
136+
supportsNativeTools: true,
131137
supportsPromptCache: true,
132138
inputPrice: 0.3,
133139
outputPrice: 2.5,
@@ -140,6 +146,7 @@ export const geminiModels = {
140146
maxTokens: 64_000,
141147
contextWindow: 1_048_576,
142148
supportsImages: true,
149+
supportsNativeTools: true,
143150
supportsPromptCache: true,
144151
inputPrice: 0.3,
145152
outputPrice: 2.5,
@@ -154,6 +161,7 @@ export const geminiModels = {
154161
maxTokens: 65_536,
155162
contextWindow: 1_048_576,
156163
supportsImages: true,
164+
supportsNativeTools: true,
157165
supportsPromptCache: true,
158166
inputPrice: 0.1,
159167
outputPrice: 0.4,
@@ -166,6 +174,7 @@ export const geminiModels = {
166174
maxTokens: 65_536,
167175
contextWindow: 1_048_576,
168176
supportsImages: true,
177+
supportsNativeTools: true,
169178
supportsPromptCache: true,
170179
inputPrice: 0.1,
171180
outputPrice: 0.4,

src/api/providers/gemini.ts

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import {
55
type GenerateContentParameters,
66
type GenerateContentConfig,
77
type GroundingMetadata,
8+
FunctionCallingConfigMode,
9+
Content,
810
} from "@google/genai"
911
import type { JWTInput } from "google-auth-library"
1012

@@ -92,9 +94,9 @@ export class GeminiHandler extends BaseProvider implements SingleCompletionHandl
9294
return true
9395
})
9496

95-
const contents = geminiMessages.map((message) =>
96-
convertAnthropicMessageToGemini(message, { includeThoughtSignatures }),
97-
)
97+
const contents: Content[] = geminiMessages
98+
.map((message) => convertAnthropicMessageToGemini(message, { includeThoughtSignatures }))
99+
.flat()
98100

99101
const tools: GenerateContentConfig["tools"] = []
100102
if (this.options.enableUrlContext) {
@@ -105,6 +107,16 @@ export class GeminiHandler extends BaseProvider implements SingleCompletionHandl
105107
tools.push({ googleSearch: {} })
106108
}
107109

110+
if (metadata?.tools && metadata.tools.length > 0) {
111+
tools.push({
112+
functionDeclarations: metadata.tools.map((tool) => ({
113+
name: (tool as any).function.name,
114+
description: (tool as any).function.description,
115+
parametersJsonSchema: (tool as any).function.parameters,
116+
})),
117+
})
118+
}
119+
108120
// Determine temperature respecting model capabilities and defaults:
109121
// - If supportsTemperature is explicitly false, ignore user overrides
110122
// and pin to the model's defaultTemperature (or omit if undefined).
@@ -124,6 +136,25 @@ export class GeminiHandler extends BaseProvider implements SingleCompletionHandl
124136
...(tools.length > 0 ? { tools } : {}),
125137
}
126138

139+
if (metadata?.tool_choice) {
140+
config.toolConfig = {
141+
functionCallingConfig: {
142+
mode:
143+
metadata.tool_choice === "auto"
144+
? FunctionCallingConfigMode.AUTO
145+
: metadata.tool_choice === "required"
146+
? FunctionCallingConfigMode.ANY
147+
: metadata.tool_choice === "none"
148+
? FunctionCallingConfigMode.NONE
149+
: FunctionCallingConfigMode.ANY,
150+
allowedFunctionNames:
151+
typeof metadata.tool_choice === "object" && "function" in metadata.tool_choice
152+
? [metadata.tool_choice.function.name]
153+
: undefined,
154+
},
155+
}
156+
}
157+
127158
const params: GenerateContentParameters = { model, contents, config }
128159

129160
try {
@@ -151,6 +182,7 @@ export class GeminiHandler extends BaseProvider implements SingleCompletionHandl
151182
thought?: boolean
152183
text?: string
153184
thoughtSignature?: string
185+
functionCall?: { name: string; args: Record<string, unknown> }
154186
}>) {
155187
// Capture thought signatures so they can be persisted into API history.
156188
const thoughtSignature = part.thoughtSignature
@@ -166,6 +198,13 @@ export class GeminiHandler extends BaseProvider implements SingleCompletionHandl
166198
if (part.text) {
167199
yield { type: "reasoning", text: part.text }
168200
}
201+
} else if (part.functionCall) {
202+
yield {
203+
type: "tool_call",
204+
id: part.functionCall.name, // Gemini doesn't provide call IDs, so we use the function name
205+
name: part.functionCall.name,
206+
arguments: JSON.stringify(part.functionCall.args),
207+
}
169208
} else {
170209
// This is regular content
171210
if (part.text) {
@@ -343,7 +382,10 @@ export class GeminiHandler extends BaseProvider implements SingleCompletionHandl
343382
const countTokensRequest = {
344383
model,
345384
// Token counting does not need encrypted continuation; always drop thoughtSignature.
346-
contents: convertAnthropicContentToGemini(content, { includeThoughtSignatures: false }),
385+
contents: convertAnthropicMessageToGemini(
386+
{ role: "user", content },
387+
{ includeThoughtSignatures: false },
388+
),
347389
}
348390

349391
const response = await this.client.models.countTokens(countTokensRequest)

src/api/transform/gemini-format.ts

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -105,9 +105,37 @@ export function convertAnthropicContentToGemini(
105105
export function convertAnthropicMessageToGemini(
106106
message: Anthropic.Messages.MessageParam,
107107
options?: { includeThoughtSignatures?: boolean },
108-
): Content {
109-
return {
110-
role: message.role === "assistant" ? "model" : "user",
111-
parts: convertAnthropicContentToGemini(message.content, options),
108+
): Content | Content[] {
109+
const content = Array.isArray(message.content) ? message.content : [{ type: "text", text: message.content ?? "" }]
110+
const toolUseParts = content.filter((block) => block.type === "tool_use") as Anthropic.ToolUseBlock[]
111+
const toolResultParts = content.filter((block) => block.type === "tool_result") as Anthropic.ToolResultBlockParam[]
112+
const otherParts = content.filter((block) => block.type !== "tool_use" && block.type !== "tool_result") as (
113+
| Anthropic.TextBlockParam
114+
| Anthropic.ImageBlockParam
115+
)[]
116+
117+
const contents: Content[] = []
118+
119+
if (otherParts.length > 0) {
120+
contents.push({
121+
role: message.role === "assistant" ? "model" : "user",
122+
parts: convertAnthropicContentToGemini(otherParts, options),
123+
})
112124
}
125+
126+
if (toolUseParts.length > 0) {
127+
contents.push({
128+
role: "model",
129+
parts: convertAnthropicContentToGemini(toolUseParts, options),
130+
})
131+
}
132+
133+
if (toolResultParts.length > 0) {
134+
contents.push({
135+
role: "user",
136+
parts: convertAnthropicContentToGemini(toolResultParts, options),
137+
})
138+
}
139+
140+
return contents
113141
}

0 commit comments

Comments
 (0)