Skip to content

Commit 5428e54

Browse files
committed
feat: enable native tool calling for gemini provider
1 parent 7c07945 commit 5428e54

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
@@ -35,6 +35,7 @@ export const geminiModels = {
3535
maxTokens: 64_000,
3636
contextWindow: 1_048_576,
3737
supportsImages: true,
38+
supportsNativeTools: true,
3839
supportsPromptCache: true,
3940
inputPrice: 2.5, // This is the pricing for prompts above 200k tokens.
4041
outputPrice: 15,
@@ -62,6 +63,7 @@ export const geminiModels = {
6263
maxTokens: 65_535,
6364
contextWindow: 1_048_576,
6465
supportsImages: true,
66+
supportsNativeTools: true,
6567
supportsPromptCache: true,
6668
inputPrice: 2.5, // This is the pricing for prompts above 200k tokens.
6769
outputPrice: 15,
@@ -88,6 +90,7 @@ export const geminiModels = {
8890
maxTokens: 65_535,
8991
contextWindow: 1_048_576,
9092
supportsImages: true,
93+
supportsNativeTools: true,
9194
supportsPromptCache: true,
9295
inputPrice: 2.5, // This is the pricing for prompts above 200k tokens.
9396
outputPrice: 15,
@@ -112,6 +115,7 @@ export const geminiModels = {
112115
maxTokens: 65_535,
113116
contextWindow: 1_048_576,
114117
supportsImages: true,
118+
supportsNativeTools: true,
115119
supportsPromptCache: true,
116120
inputPrice: 2.5, // This is the pricing for prompts above 200k tokens.
117121
outputPrice: 15,
@@ -140,6 +144,7 @@ export const geminiModels = {
140144
maxTokens: 65_536,
141145
contextWindow: 1_048_576,
142146
supportsImages: true,
147+
supportsNativeTools: true,
143148
supportsPromptCache: true,
144149
inputPrice: 0.3,
145150
outputPrice: 2.5,
@@ -152,6 +157,7 @@ export const geminiModels = {
152157
maxTokens: 65_536,
153158
contextWindow: 1_048_576,
154159
supportsImages: true,
160+
supportsNativeTools: true,
155161
supportsPromptCache: true,
156162
inputPrice: 0.3,
157163
outputPrice: 2.5,
@@ -164,6 +170,7 @@ export const geminiModels = {
164170
maxTokens: 64_000,
165171
contextWindow: 1_048_576,
166172
supportsImages: true,
173+
supportsNativeTools: true,
167174
supportsPromptCache: true,
168175
inputPrice: 0.3,
169176
outputPrice: 2.5,
@@ -178,6 +185,7 @@ export const geminiModels = {
178185
maxTokens: 65_536,
179186
contextWindow: 1_048_576,
180187
supportsImages: true,
188+
supportsNativeTools: true,
181189
supportsPromptCache: true,
182190
inputPrice: 0.1,
183191
outputPrice: 0.4,
@@ -190,6 +198,7 @@ export const geminiModels = {
190198
maxTokens: 65_536,
191199
contextWindow: 1_048_576,
192200
supportsImages: true,
201+
supportsNativeTools: true,
193202
supportsPromptCache: true,
194203
inputPrice: 0.1,
195204
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

@@ -101,9 +103,9 @@ export class GeminiHandler extends BaseProvider implements SingleCompletionHandl
101103
return true
102104
})
103105

104-
const contents = geminiMessages.map((message) =>
105-
convertAnthropicMessageToGemini(message, { includeThoughtSignatures }),
106-
)
106+
const contents: Content[] = geminiMessages
107+
.map((message) => convertAnthropicMessageToGemini(message, { includeThoughtSignatures }))
108+
.flat()
107109

108110
const tools: GenerateContentConfig["tools"] = []
109111
if (this.options.enableUrlContext) {
@@ -114,6 +116,16 @@ export class GeminiHandler extends BaseProvider implements SingleCompletionHandl
114116
tools.push({ googleSearch: {} })
115117
}
116118

119+
if (metadata?.tools && metadata.tools.length > 0) {
120+
tools.push({
121+
functionDeclarations: metadata.tools.map((tool) => ({
122+
name: (tool as any).function.name,
123+
description: (tool as any).function.description,
124+
parametersJsonSchema: (tool as any).function.parameters,
125+
})),
126+
})
127+
}
128+
117129
// Determine temperature respecting model capabilities and defaults:
118130
// - If supportsTemperature is explicitly false, ignore user overrides
119131
// and pin to the model's defaultTemperature (or omit if undefined).
@@ -133,6 +145,25 @@ export class GeminiHandler extends BaseProvider implements SingleCompletionHandl
133145
...(tools.length > 0 ? { tools } : {}),
134146
}
135147

148+
if (metadata?.tool_choice) {
149+
config.toolConfig = {
150+
functionCallingConfig: {
151+
mode:
152+
metadata.tool_choice === "auto"
153+
? FunctionCallingConfigMode.AUTO
154+
: metadata.tool_choice === "required"
155+
? FunctionCallingConfigMode.ANY
156+
: metadata.tool_choice === "none"
157+
? FunctionCallingConfigMode.NONE
158+
: FunctionCallingConfigMode.ANY,
159+
allowedFunctionNames:
160+
typeof metadata.tool_choice === "object" && "function" in metadata.tool_choice
161+
? [metadata.tool_choice.function.name]
162+
: undefined,
163+
},
164+
}
165+
}
166+
136167
const params: GenerateContentParameters = { model, contents, config }
137168
try {
138169
const result = await this.client.models.generateContentStream(params)
@@ -159,6 +190,7 @@ export class GeminiHandler extends BaseProvider implements SingleCompletionHandl
159190
thought?: boolean
160191
text?: string
161192
thoughtSignature?: string
193+
functionCall?: { name: string; args: Record<string, unknown> }
162194
}>) {
163195
// Capture thought signatures so they can be persisted into API history.
164196
const thoughtSignature = part.thoughtSignature
@@ -174,6 +206,13 @@ export class GeminiHandler extends BaseProvider implements SingleCompletionHandl
174206
if (part.text) {
175207
yield { type: "reasoning", text: part.text }
176208
}
209+
} else if (part.functionCall) {
210+
yield {
211+
type: "tool_call",
212+
id: part.functionCall.name, // Gemini doesn't provide call IDs, so we use the function name
213+
name: part.functionCall.name,
214+
arguments: JSON.stringify(part.functionCall.args),
215+
}
177216
} else {
178217
// This is regular content
179218
if (part.text) {
@@ -351,7 +390,10 @@ export class GeminiHandler extends BaseProvider implements SingleCompletionHandl
351390
const countTokensRequest = {
352391
model,
353392
// Token counting does not need encrypted continuation; always drop thoughtSignature.
354-
contents: convertAnthropicContentToGemini(content, { includeThoughtSignatures: false }),
393+
contents: convertAnthropicMessageToGemini(
394+
{ role: "user", content },
395+
{ includeThoughtSignatures: false },
396+
),
355397
}
356398

357399
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)