Skip to content

Commit d6bd20b

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

File tree

3 files changed

+86
-8
lines changed

3 files changed

+86
-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: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
type GenerateContentParameters,
66
type GenerateContentConfig,
77
type GroundingMetadata,
8+
FunctionCallingConfigMode,
89
} from "@google/genai"
910
import type { JWTInput } from "google-auth-library"
1011

@@ -92,9 +93,9 @@ export class GeminiHandler extends BaseProvider implements SingleCompletionHandl
9293
return true
9394
})
9495

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

99100
const tools: GenerateContentConfig["tools"] = []
100101
if (this.options.enableUrlContext) {
@@ -105,6 +106,16 @@ export class GeminiHandler extends BaseProvider implements SingleCompletionHandl
105106
tools.push({ googleSearch: {} })
106107
}
107108

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

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

129159
try {
@@ -151,6 +181,7 @@ export class GeminiHandler extends BaseProvider implements SingleCompletionHandl
151181
thought?: boolean
152182
text?: string
153183
thoughtSignature?: string
184+
functionCall?: { name: string; args: Record<string, unknown> }
154185
}>) {
155186
// Capture thought signatures so they can be persisted into API history.
156187
const thoughtSignature = part.thoughtSignature
@@ -166,6 +197,13 @@ export class GeminiHandler extends BaseProvider implements SingleCompletionHandl
166197
if (part.text) {
167198
yield { type: "reasoning", text: part.text }
168199
}
200+
} else if (part.functionCall) {
201+
yield {
202+
type: "tool_call",
203+
id: part.functionCall.name, // Gemini doesn't provide call IDs, so we use the function name
204+
name: part.functionCall.name,
205+
arguments: JSON.stringify(part.functionCall.args),
206+
}
169207
} else {
170208
// This is regular content
171209
if (part.text) {
@@ -343,7 +381,10 @@ export class GeminiHandler extends BaseProvider implements SingleCompletionHandl
343381
const countTokensRequest = {
344382
model,
345383
// Token counting does not need encrypted continuation; always drop thoughtSignature.
346-
contents: convertAnthropicContentToGemini(content, { includeThoughtSignatures: false }),
384+
contents: convertAnthropicMessageToGemini(
385+
{ role: "user", content },
386+
{ includeThoughtSignatures: false },
387+
),
347388
}
348389

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