Improve Google Gemini defaults, temperature, and cost reporting (#9327)

This commit is contained in:
Hannes Rudolph 2025-11-17 23:38:38 -07:00 committed by GitHub
parent aa2a17fce3
commit f7c2e8d164
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 379 additions and 331 deletions

View file

@ -3,136 +3,37 @@ import type { ModelInfo } from "../model.js"
// https://ai.google.dev/gemini-api/docs/models/gemini
export type GeminiModelId = keyof typeof geminiModels
export const geminiDefaultModelId: GeminiModelId = "gemini-2.0-flash-001"
export const geminiDefaultModelId: GeminiModelId = "gemini-2.5-pro"
export const geminiModels = {
// Latest models (pointing to the most recent stable versions)
"gemini-flash-latest": {
maxTokens: 65_536,
contextWindow: 1_048_576,
supportsImages: true,
supportsPromptCache: true,
inputPrice: 0.3,
outputPrice: 2.5,
cacheReadsPrice: 0.075,
cacheWritesPrice: 1.0,
maxThinkingTokens: 24_576,
supportsReasoningBudget: true,
},
"gemini-flash-lite-latest": {
maxTokens: 65_536,
contextWindow: 1_048_576,
supportsImages: true,
supportsPromptCache: true,
inputPrice: 0.1,
outputPrice: 0.4,
cacheReadsPrice: 0.025,
cacheWritesPrice: 1.0,
supportsReasoningBudget: true,
maxThinkingTokens: 24_576,
},
// 2.5 Flash models (09-2025 versions - most recent)
"gemini-2.5-flash-preview-09-2025": {
maxTokens: 65_536,
contextWindow: 1_048_576,
supportsImages: true,
supportsPromptCache: true,
inputPrice: 0.3,
outputPrice: 2.5,
cacheReadsPrice: 0.075,
cacheWritesPrice: 1.0,
maxThinkingTokens: 24_576,
supportsReasoningBudget: true,
},
"gemini-2.5-flash-lite-preview-09-2025": {
maxTokens: 65_536,
contextWindow: 1_048_576,
supportsImages: true,
supportsPromptCache: true,
inputPrice: 0.1,
outputPrice: 0.4,
cacheReadsPrice: 0.025,
cacheWritesPrice: 1.0,
supportsReasoningBudget: true,
maxThinkingTokens: 24_576,
},
// 2.5 Flash models (06-17 version)
"gemini-2.5-flash-lite-preview-06-17": {
maxTokens: 64_000,
contextWindow: 1_048_576,
supportsImages: true,
supportsPromptCache: true,
inputPrice: 0.1,
outputPrice: 0.4,
cacheReadsPrice: 0.025,
cacheWritesPrice: 1.0,
supportsReasoningBudget: true,
maxThinkingTokens: 24_576,
},
// 2.5 Flash models (05-20 versions)
"gemini-2.5-flash-preview-05-20:thinking": {
maxTokens: 65_535,
contextWindow: 1_048_576,
supportsImages: true,
supportsPromptCache: true,
inputPrice: 0.15,
outputPrice: 3.5,
cacheReadsPrice: 0.0375,
cacheWritesPrice: 1.0,
maxThinkingTokens: 24_576,
supportsReasoningBudget: true,
requiredReasoningBudget: true,
},
"gemini-2.5-flash-preview-05-20": {
maxTokens: 65_535,
contextWindow: 1_048_576,
supportsImages: true,
supportsPromptCache: true,
inputPrice: 0.15,
outputPrice: 0.6,
cacheReadsPrice: 0.0375,
cacheWritesPrice: 1.0,
},
// 2.5 Flash models (04-17 versions)
"gemini-2.5-flash-preview-04-17:thinking": {
maxTokens: 65_535,
contextWindow: 1_048_576,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 0.15,
outputPrice: 3.5,
maxThinkingTokens: 24_576,
supportsReasoningBudget: true,
requiredReasoningBudget: true,
},
"gemini-2.5-flash-preview-04-17": {
maxTokens: 65_535,
contextWindow: 1_048_576,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 0.15,
outputPrice: 0.6,
},
// 2.5 Flash stable
"gemini-2.5-flash": {
maxTokens: 64_000,
contextWindow: 1_048_576,
supportsImages: true,
supportsPromptCache: true,
inputPrice: 0.3,
outputPrice: 2.5,
cacheReadsPrice: 0.075,
cacheWritesPrice: 1.0,
maxThinkingTokens: 24_576,
supportsReasoningBudget: true,
},
// 2.5 Pro models
"gemini-2.5-pro": {
maxTokens: 64_000,
contextWindow: 1_048_576,
supportsImages: true,
supportsPromptCache: true,
inputPrice: 2.5, // This is the pricing for prompts above 200k tokens.
outputPrice: 15,
cacheReadsPrice: 0.625,
cacheWritesPrice: 4.5,
maxThinkingTokens: 32_768,
supportsReasoningBudget: true,
requiredReasoningBudget: true,
tiers: [
{
contextWindow: 200_000,
inputPrice: 1.25,
outputPrice: 10,
cacheReadsPrice: 0.31,
},
{
contextWindow: Infinity,
inputPrice: 2.5,
outputPrice: 15,
cacheReadsPrice: 0.625,
},
],
},
"gemini-2.5-pro-preview-06-05": {
maxTokens: 65_535,
contextWindow: 1_048_576,
@ -209,163 +110,68 @@ export const geminiModels = {
},
],
},
"gemini-2.5-pro-exp-03-25": {
maxTokens: 65_535,
// 2.5 Flash models
"gemini-flash-latest": {
maxTokens: 65_536,
contextWindow: 1_048_576,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
supportsPromptCache: true,
inputPrice: 0.3,
outputPrice: 2.5,
cacheReadsPrice: 0.075,
cacheWritesPrice: 1.0,
maxThinkingTokens: 24_576,
supportsReasoningBudget: true,
},
"gemini-2.5-pro": {
"gemini-2.5-flash-preview-09-2025": {
maxTokens: 65_536,
contextWindow: 1_048_576,
supportsImages: true,
supportsPromptCache: true,
inputPrice: 0.3,
outputPrice: 2.5,
cacheReadsPrice: 0.075,
cacheWritesPrice: 1.0,
maxThinkingTokens: 24_576,
supportsReasoningBudget: true,
},
"gemini-2.5-flash": {
maxTokens: 64_000,
contextWindow: 1_048_576,
supportsImages: true,
supportsPromptCache: true,
inputPrice: 2.5, // This is the pricing for prompts above 200k tokens.
outputPrice: 15,
cacheReadsPrice: 0.625,
cacheWritesPrice: 4.5,
maxThinkingTokens: 32_768,
inputPrice: 0.3,
outputPrice: 2.5,
cacheReadsPrice: 0.075,
cacheWritesPrice: 1.0,
maxThinkingTokens: 24_576,
supportsReasoningBudget: true,
requiredReasoningBudget: true,
tiers: [
{
contextWindow: 200_000,
inputPrice: 1.25,
outputPrice: 10,
cacheReadsPrice: 0.31,
},
{
contextWindow: Infinity,
inputPrice: 2.5,
outputPrice: 15,
cacheReadsPrice: 0.625,
},
],
},
// 2.0 Flash models
"gemini-2.0-flash-lite-preview-02-05": {
maxTokens: 8192,
contextWindow: 1_048_576,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
},
"gemini-2.0-flash-thinking-exp-01-21": {
// 2.5 Flash Lite models
"gemini-flash-lite-latest": {
maxTokens: 65_536,
contextWindow: 1_048_576,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
},
"gemini-2.0-flash-thinking-exp-1219": {
maxTokens: 8192,
contextWindow: 32_767,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
},
"gemini-2.0-flash-exp": {
maxTokens: 8192,
contextWindow: 1_048_576,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
},
"gemini-2.0-flash-001": {
maxTokens: 8192,
contextWindow: 1_048_576,
supportsImages: true,
supportsPromptCache: true,
inputPrice: 0.1,
outputPrice: 0.4,
cacheReadsPrice: 0.025,
cacheWritesPrice: 1.0,
supportsReasoningBudget: true,
maxThinkingTokens: 24_576,
},
// 2.0 Pro models
"gemini-2.0-pro-exp-02-05": {
maxTokens: 8192,
contextWindow: 2_097_152,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
},
// 1.5 Flash models
"gemini-1.5-flash-002": {
maxTokens: 8192,
"gemini-2.5-flash-lite-preview-09-2025": {
maxTokens: 65_536,
contextWindow: 1_048_576,
supportsImages: true,
supportsPromptCache: true,
inputPrice: 0.15, // This is the pricing for prompts above 128k tokens.
outputPrice: 0.6,
cacheReadsPrice: 0.0375,
inputPrice: 0.1,
outputPrice: 0.4,
cacheReadsPrice: 0.025,
cacheWritesPrice: 1.0,
tiers: [
{
contextWindow: 128_000,
inputPrice: 0.075,
outputPrice: 0.3,
cacheReadsPrice: 0.01875,
},
{
contextWindow: Infinity,
inputPrice: 0.15,
outputPrice: 0.6,
cacheReadsPrice: 0.0375,
},
],
},
"gemini-1.5-flash-exp-0827": {
maxTokens: 8192,
contextWindow: 1_048_576,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
},
"gemini-1.5-flash-8b-exp-0827": {
maxTokens: 8192,
contextWindow: 1_048_576,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
},
// 1.5 Pro models
"gemini-1.5-pro-002": {
maxTokens: 8192,
contextWindow: 2_097_152,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
},
"gemini-1.5-pro-exp-0827": {
maxTokens: 8192,
contextWindow: 2_097_152,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
},
// Experimental models
"gemini-exp-1206": {
maxTokens: 8192,
contextWindow: 2_097_152,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
supportsReasoningBudget: true,
maxThinkingTokens: 24_576,
},
} as const satisfies Record<string, ModelInfo>

103
pnpm-lock.yaml generated
View file

@ -627,8 +627,8 @@ importers:
specifier: ^3.922.0
version: 3.922.0
'@google/genai':
specifier: ^1.0.0
version: 1.3.0(@modelcontextprotocol/sdk@1.12.0)
specifier: ^1.29.1
version: 1.29.1(@modelcontextprotocol/sdk@1.12.0)
'@lmstudio/sdk':
specifier: ^1.1.1
version: 1.2.0
@ -1914,11 +1914,14 @@ packages:
'@floating-ui/utils@0.2.9':
resolution: {integrity: sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==}
'@google/genai@1.3.0':
resolution: {integrity: sha512-rrMzAELX4P902FUpuWy/W3NcQ7L3q/qtCzfCmGVqIce8yWpptTF9hkKsw744tvZpwqhuzD0URibcJA95wd8QFA==}
'@google/genai@1.29.1':
resolution: {integrity: sha512-Buywpq0A6xf9cOdhiWCi5KUiDBbZkjCH5xbl+xxNQRItoYQgd31p0OKyn5cUnT0YNzC/pAmszqXoOc7kncqfFQ==}
engines: {node: '>=20.0.0'}
peerDependencies:
'@modelcontextprotocol/sdk': ^1.11.0
'@modelcontextprotocol/sdk': ^1.20.1
peerDependenciesMeta:
'@modelcontextprotocol/sdk':
optional: true
'@hookform/resolvers@5.1.1':
resolution: {integrity: sha512-J/NVING3LMAEvexJkyTLjruSm7aOFx7QX21pzkiJfMoNG0wl5aFEjLTl7ay7IQb9EWY6AkrBy7tHL2Alijpdcg==}
@ -6208,10 +6211,18 @@ packages:
resolution: {integrity: sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==}
engines: {node: '>=14'}
gaxios@7.1.3:
resolution: {integrity: sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ==}
engines: {node: '>=18'}
gcp-metadata@6.1.1:
resolution: {integrity: sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==}
engines: {node: '>=14'}
gcp-metadata@8.1.2:
resolution: {integrity: sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==}
engines: {node: '>=18'}
gel@2.1.0:
resolution: {integrity: sha512-HCeRqInCt6BjbMmeghJ6BKeYwOj7WJT5Db6IWWAA3IMUUa7or7zJfTUEkUWCxiOtoXnwnm96sFK9Fr47Yh2hOA==}
engines: {node: '>= 18.0.0'}
@ -6328,6 +6339,10 @@ packages:
resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==}
engines: {node: '>=10'}
google-auth-library@10.5.0:
resolution: {integrity: sha512-7ABviyMOlX5hIVD60YOfHw4/CxOfBhyduaYB+wbFWCWoni4N7SLcV46hrVRktuBbZjFC9ONyqamZITN7q3n32w==}
engines: {node: '>=18'}
google-auth-library@9.15.1:
resolution: {integrity: sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==}
engines: {node: '>=14'}
@ -6336,6 +6351,10 @@ packages:
resolution: {integrity: sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==}
engines: {node: '>=14'}
google-logging-utils@1.1.3:
resolution: {integrity: sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==}
engines: {node: '>=14'}
gopd@1.2.0:
resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
engines: {node: '>= 0.4'}
@ -6354,6 +6373,10 @@ packages:
resolution: {integrity: sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==}
engines: {node: '>=14.0.0'}
gtoken@8.0.0:
resolution: {integrity: sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw==}
engines: {node: '>=18'}
hachure-fill@0.5.2:
resolution: {integrity: sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==}
@ -8750,6 +8773,10 @@ packages:
deprecated: Rimraf versions prior to v4 are no longer supported
hasBin: true
rimraf@5.0.10:
resolution: {integrity: sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==}
hasBin: true
rimraf@6.0.1:
resolution: {integrity: sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A==}
engines: {node: 20 || >=22}
@ -11470,16 +11497,14 @@ snapshots:
'@floating-ui/utils@0.2.9': {}
'@google/genai@1.3.0(@modelcontextprotocol/sdk@1.12.0)':
'@google/genai@1.29.1(@modelcontextprotocol/sdk@1.12.0)':
dependencies:
google-auth-library: 10.5.0
ws: 8.18.3
optionalDependencies:
'@modelcontextprotocol/sdk': 1.12.0
google-auth-library: 9.15.1
ws: 8.18.2
zod: 3.25.76
zod-to-json-schema: 3.24.5(zod@3.25.76)
transitivePeerDependencies:
- bufferutil
- encoding
- supports-color
- utf-8-validate
@ -14041,7 +14066,7 @@ snapshots:
sirv: 3.0.1
tinyglobby: 0.2.14
tinyrainbow: 2.0.0
vitest: 3.2.4(@types/debug@4.1.12)(@types/node@20.17.50)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0)
vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.2.1)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0)
'@vitest/utils@3.2.4':
dependencies:
@ -15110,8 +15135,7 @@ snapshots:
d3: 7.9.0
lodash-es: 4.17.21
data-uri-to-buffer@4.0.1:
optional: true
data-uri-to-buffer@4.0.1: {}
data-uri-to-buffer@6.0.2: {}
@ -15991,7 +16015,6 @@ snapshots:
dependencies:
node-domexception: 1.0.0
web-streams-polyfill: 3.3.3
optional: true
fflate@0.4.8: {}
@ -16085,7 +16108,6 @@ snapshots:
formdata-polyfill@4.0.10:
dependencies:
fetch-blob: 3.2.0
optional: true
forwarded@0.2.0: {}
@ -16170,6 +16192,15 @@ snapshots:
- encoding
- supports-color
gaxios@7.1.3:
dependencies:
extend: 3.0.2
https-proxy-agent: 7.0.6
node-fetch: 3.3.2
rimraf: 5.0.10
transitivePeerDependencies:
- supports-color
gcp-metadata@6.1.1:
dependencies:
gaxios: 6.7.1
@ -16179,6 +16210,14 @@ snapshots:
- encoding
- supports-color
gcp-metadata@8.1.2:
dependencies:
gaxios: 7.1.3
google-logging-utils: 1.1.3
json-bigint: 1.0.0
transitivePeerDependencies:
- supports-color
gel@2.1.0:
dependencies:
'@petamoriken/float16': 3.9.3
@ -16316,6 +16355,18 @@ snapshots:
merge2: 1.4.1
slash: 3.0.0
google-auth-library@10.5.0:
dependencies:
base64-js: 1.5.1
ecdsa-sig-formatter: 1.0.11
gaxios: 7.1.3
gcp-metadata: 8.1.2
google-logging-utils: 1.1.3
gtoken: 8.0.0
jws: 4.0.0
transitivePeerDependencies:
- supports-color
google-auth-library@9.15.1:
dependencies:
base64-js: 1.5.1
@ -16330,6 +16381,8 @@ snapshots:
google-logging-utils@0.0.2: {}
google-logging-utils@1.1.3: {}
gopd@1.2.0: {}
graceful-fs@4.2.11: {}
@ -16351,6 +16404,13 @@ snapshots:
- encoding
- supports-color
gtoken@8.0.0:
dependencies:
gaxios: 7.1.3
jws: 4.0.0
transitivePeerDependencies:
- supports-color
hachure-fill@0.5.2: {}
harmony-reflect@1.6.2: {}
@ -18141,7 +18201,6 @@ snapshots:
data-uri-to-buffer: 4.0.1
fetch-blob: 3.2.0
formdata-polyfill: 4.0.10
optional: true
node-ipc@12.0.0:
dependencies:
@ -19211,6 +19270,10 @@ snapshots:
dependencies:
glob: 7.2.3
rimraf@5.0.10:
dependencies:
glob: 10.4.5
rimraf@6.0.1:
dependencies:
glob: 11.0.3
@ -20788,8 +20851,7 @@ snapshots:
web-namespaces@2.0.1: {}
web-streams-polyfill@3.3.3:
optional: true
web-streams-polyfill@3.3.3: {}
web-streams-polyfill@4.0.0-beta.3: {}
@ -20933,8 +20995,7 @@ snapshots:
ws@8.18.2: {}
ws@8.18.3:
optional: true
ws@8.18.3: {}
xml-name-validator@5.0.0: {}

View file

@ -7,7 +7,7 @@ import { type ModelInfo, geminiDefaultModelId } from "@roo-code/types"
import { t } from "i18next"
import { GeminiHandler } from "../gemini"
const GEMINI_20_FLASH_THINKING_NAME = "gemini-2.0-flash-thinking-exp-1219"
const GEMINI_MODEL_NAME = geminiDefaultModelId
describe("GeminiHandler", () => {
let handler: GeminiHandler
@ -20,7 +20,7 @@ describe("GeminiHandler", () => {
handler = new GeminiHandler({
apiKey: "test-key",
apiModelId: GEMINI_20_FLASH_THINKING_NAME,
apiModelId: GEMINI_MODEL_NAME,
geminiApiKey: "test-key",
})
@ -37,7 +37,7 @@ describe("GeminiHandler", () => {
describe("constructor", () => {
it("should initialize with provided config", () => {
expect(handler["options"].geminiApiKey).toBe("test-key")
expect(handler["options"].apiModelId).toBe(GEMINI_20_FLASH_THINKING_NAME)
expect(handler["options"].apiModelId).toBe(GEMINI_MODEL_NAME)
})
})
@ -76,14 +76,14 @@ describe("GeminiHandler", () => {
expect(chunks.length).toBe(3)
expect(chunks[0]).toEqual({ type: "text", text: "Hello" })
expect(chunks[1]).toEqual({ type: "text", text: " world!" })
expect(chunks[2]).toEqual({ type: "usage", inputTokens: 10, outputTokens: 5 })
expect(chunks[2]).toMatchObject({ type: "usage", inputTokens: 10, outputTokens: 5 })
// Verify the call to generateContentStream
expect(handler["client"].models.generateContentStream).toHaveBeenCalledWith(
expect.objectContaining({
model: GEMINI_20_FLASH_THINKING_NAME,
model: GEMINI_MODEL_NAME,
config: expect.objectContaining({
temperature: 0,
temperature: 1,
systemInstruction: systemPrompt,
}),
}),
@ -116,11 +116,11 @@ describe("GeminiHandler", () => {
// Verify the call to generateContent
expect(handler["client"].models.generateContent).toHaveBeenCalledWith({
model: GEMINI_20_FLASH_THINKING_NAME,
model: GEMINI_MODEL_NAME,
contents: [{ role: "user", parts: [{ text: "Test prompt" }] }],
config: {
httpOptions: undefined,
temperature: 0,
temperature: 1,
},
})
})
@ -148,10 +148,8 @@ describe("GeminiHandler", () => {
describe("getModel", () => {
it("should return correct model info", () => {
const modelInfo = handler.getModel()
expect(modelInfo.id).toBe(GEMINI_20_FLASH_THINKING_NAME)
expect(modelInfo.id).toBe(GEMINI_MODEL_NAME)
expect(modelInfo.info).toBeDefined()
expect(modelInfo.info.maxTokens).toBe(8192)
expect(modelInfo.info.contextWindow).toBe(32_767)
})
it("should return default model if invalid model specified", () => {

View file

@ -95,7 +95,7 @@ describe("VertexHandler", () => {
model: expect.any(String),
contents: [{ role: "user", parts: [{ text: "Test prompt" }] }],
config: expect.objectContaining({
temperature: 0,
temperature: 1,
}),
}),
)

View file

@ -29,6 +29,8 @@ export class GeminiHandler extends BaseProvider implements SingleCompletionHandl
protected options: ApiHandlerOptions
private client: GoogleGenAI
private lastThoughtSignature?: string
private lastResponseId?: string
constructor({ isVertex, ...options }: GeminiHandlerOptions) {
super()
@ -66,8 +68,33 @@ export class GeminiHandler extends BaseProvider implements SingleCompletionHandl
metadata?: ApiHandlerCreateMessageMetadata,
): ApiStream {
const { id: model, info, reasoning: thinkingConfig, maxTokens } = this.getModel()
// Reset per-request metadata that we persist into apiConversationHistory.
this.lastThoughtSignature = undefined
this.lastResponseId = undefined
const contents = messages.map(convertAnthropicMessageToGemini)
// Only forward encrypted reasoning continuations (thoughtSignature) when we are
// using effort-based reasoning (thinkingLevel). Budget-only configs should NOT
// send thoughtSignature parts back to Gemini.
const includeThoughtSignatures = Boolean(thinkingConfig?.thinkingLevel)
// The message list can include provider-specific meta entries such as
// `{ type: "reasoning", ... }` that are intended only for providers like
// openai-native. Gemini should never see those; they are not valid
// Anthropic.MessageParam values and will cause failures (e.g. missing
// `content` for the converter). Filter them out here.
type ReasoningMetaLike = { type?: string }
const geminiMessages = messages.filter((message): message is Anthropic.Messages.MessageParam => {
const meta = message as ReasoningMetaLike
if (meta.type === "reasoning") {
return false
}
return true
})
const contents = geminiMessages.map((message) =>
convertAnthropicMessageToGemini(message, { includeThoughtSignatures }),
)
const tools: GenerateContentConfig["tools"] = []
if (this.options.enableUrlContext) {
@ -78,12 +105,22 @@ export class GeminiHandler extends BaseProvider implements SingleCompletionHandl
tools.push({ googleSearch: {} })
}
// Determine temperature respecting model capabilities and defaults:
// - If supportsTemperature is explicitly false, ignore user overrides
// and pin to the model's defaultTemperature (or omit if undefined).
// - Otherwise, allow the user setting to override, falling back to model default,
// then to 1 for Gemini provider default.
const supportsTemperature = info.supportsTemperature !== false
const temperatureConfig: number | undefined = supportsTemperature
? (this.options.modelTemperature ?? info.defaultTemperature ?? 1)
: info.defaultTemperature
const config: GenerateContentConfig = {
systemInstruction,
httpOptions: this.options.googleGeminiBaseUrl ? { baseUrl: this.options.googleGeminiBaseUrl } : undefined,
thinkingConfig,
maxOutputTokens: this.options.modelMaxTokens ?? maxTokens ?? undefined,
temperature: this.options.modelTemperature ?? 0,
temperature: temperatureConfig,
...(tools.length > 0 ? { tools } : {}),
}
@ -94,8 +131,13 @@ export class GeminiHandler extends BaseProvider implements SingleCompletionHandl
let lastUsageMetadata: GenerateContentResponseUsageMetadata | undefined
let pendingGroundingMetadata: GroundingMetadata | undefined
let finalResponse: { responseId?: string } | undefined
for await (const chunk of result) {
// Track the final structured response (per SDK pattern: candidate.finishReason)
if (chunk.candidates && chunk.candidates[0]?.finishReason) {
finalResponse = chunk as { responseId?: string }
}
// Process candidates and their parts to separate thoughts from content
if (chunk.candidates && chunk.candidates.length > 0) {
const candidate = chunk.candidates[0]
@ -105,7 +147,20 @@ export class GeminiHandler extends BaseProvider implements SingleCompletionHandl
}
if (candidate.content && candidate.content.parts) {
for (const part of candidate.content.parts) {
for (const part of candidate.content.parts as Array<{
thought?: boolean
text?: string
thoughtSignature?: string
}>) {
// Capture thought signatures so they can be persisted into API history.
const thoughtSignature = part.thoughtSignature
// Only persist encrypted reasoning when an effort-based thinking level is set
// (i.e. thinkingConfig.thinkingLevel is present). Budget-based configs that only
// set thinkingBudget should NOT trigger encrypted continuation.
if (thinkingConfig?.thinkingLevel && thoughtSignature) {
this.lastThoughtSignature = thoughtSignature
}
if (part.thought) {
// This is a thinking/reasoning part
if (part.text) {
@ -131,6 +186,12 @@ export class GeminiHandler extends BaseProvider implements SingleCompletionHandl
}
}
if (finalResponse?.responseId) {
// Capture responseId so Task.addToApiConversationHistory can store it
// alongside the assistant message in api_history.json.
this.lastResponseId = finalResponse.responseId
}
if (pendingGroundingMetadata) {
const sources = this.extractGroundingSources(pendingGroundingMetadata)
if (sources.length > 0) {
@ -150,7 +211,13 @@ export class GeminiHandler extends BaseProvider implements SingleCompletionHandl
outputTokens,
cacheReadTokens,
reasoningTokens,
totalCost: this.calculateCost({ info, inputTokens, outputTokens, cacheReadTokens }),
totalCost: this.calculateCost({
info,
inputTokens,
outputTokens,
cacheReadTokens,
reasoningTokens,
}),
}
}
} catch (error) {
@ -166,7 +233,14 @@ export class GeminiHandler extends BaseProvider implements SingleCompletionHandl
const modelId = this.options.apiModelId
let id = modelId && modelId in geminiModels ? (modelId as GeminiModelId) : geminiDefaultModelId
let info: ModelInfo = geminiModels[id]
const params = getModelParams({ format: "gemini", modelId: id, model: info, settings: this.options })
const params = getModelParams({
format: "gemini",
modelId: id,
model: info,
settings: this.options,
defaultTemperature: info.defaultTemperature ?? 1,
})
// The `:thinking` suffix indicates that the model is a "Hybrid"
// reasoning model and that reasoning is required to be enabled.
@ -211,7 +285,7 @@ export class GeminiHandler extends BaseProvider implements SingleCompletionHandl
async completePrompt(prompt: string): Promise<string> {
try {
const { id: model } = this.getModel()
const { id: model, info } = this.getModel()
const tools: GenerateContentConfig["tools"] = []
if (this.options.enableUrlContext) {
@ -220,19 +294,27 @@ export class GeminiHandler extends BaseProvider implements SingleCompletionHandl
if (this.options.enableGrounding) {
tools.push({ googleSearch: {} })
}
const supportsTemperature = info.supportsTemperature !== false
const temperatureConfig: number | undefined = supportsTemperature
? (this.options.modelTemperature ?? info.defaultTemperature ?? 1)
: info.defaultTemperature
const promptConfig: GenerateContentConfig = {
httpOptions: this.options.googleGeminiBaseUrl
? { baseUrl: this.options.googleGeminiBaseUrl }
: undefined,
temperature: this.options.modelTemperature ?? 0,
temperature: temperatureConfig,
...(tools.length > 0 ? { tools } : {}),
}
const result = await this.client.models.generateContent({
const request = {
model,
contents: [{ role: "user", parts: [{ text: prompt }] }],
config: promptConfig,
})
}
const result = await this.client.models.generateContent(request)
let text = result.text ?? ""
@ -258,10 +340,13 @@ export class GeminiHandler extends BaseProvider implements SingleCompletionHandl
try {
const { id: model } = this.getModel()
const response = await this.client.models.countTokens({
const countTokensRequest = {
model,
contents: convertAnthropicContentToGemini(content),
})
// Token counting does not need encrypted continuation; always drop thoughtSignature.
contents: convertAnthropicContentToGemini(content, { includeThoughtSignatures: false }),
}
const response = await this.client.models.countTokens(countTokensRequest)
if (response.totalTokens === undefined) {
console.warn("Gemini token counting returned undefined, using fallback")
@ -275,16 +360,26 @@ export class GeminiHandler extends BaseProvider implements SingleCompletionHandl
}
}
public getThoughtSignature(): string | undefined {
return this.lastThoughtSignature
}
public getResponseId(): string | undefined {
return this.lastResponseId
}
public calculateCost({
info,
inputTokens,
outputTokens,
cacheReadTokens = 0,
reasoningTokens = 0,
}: {
info: ModelInfo
inputTokens: number
outputTokens: number
cacheReadTokens?: number
reasoningTokens?: number
}) {
// For models with tiered pricing, prices might only be defined in tiers
let inputPrice = info.inputPrice
@ -316,23 +411,24 @@ export class GeminiHandler extends BaseProvider implements SingleCompletionHandl
// Subtract the cached input tokens from the total input tokens.
const uncachedInputTokens = inputTokens - cacheReadTokens
// Bill both completion and reasoning ("thoughts") tokens as output.
const billedOutputTokens = outputTokens + reasoningTokens
let cacheReadCost = cacheReadTokens > 0 ? cacheReadsPrice * (cacheReadTokens / 1_000_000) : 0
const inputTokensCost = inputPrice * (uncachedInputTokens / 1_000_000)
const outputTokensCost = outputPrice * (outputTokens / 1_000_000)
const outputTokensCost = outputPrice * (billedOutputTokens / 1_000_000)
const totalCost = inputTokensCost + outputTokensCost + cacheReadCost
const trace: Record<string, { price: number; tokens: number; cost: number }> = {
input: { price: inputPrice, tokens: uncachedInputTokens, cost: inputTokensCost },
output: { price: outputPrice, tokens: outputTokens, cost: outputTokensCost },
output: { price: outputPrice, tokens: billedOutputTokens, cost: outputTokensCost },
}
if (cacheReadTokens > 0) {
trace.cacheRead = { price: cacheReadsPrice, tokens: cacheReadTokens, cost: cacheReadCost }
}
// console.log(`[GeminiHandler] calculateCost -> ${totalCost}`, trace)
return totalCost
}
}

View file

@ -1,12 +1,44 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { Content, Part } from "@google/genai"
export function convertAnthropicContentToGemini(content: string | Anthropic.ContentBlockParam[]): Part[] {
type ThoughtSignatureContentBlock = {
type: "thoughtSignature"
thoughtSignature?: string
}
type ExtendedContentBlockParam = Anthropic.ContentBlockParam | ThoughtSignatureContentBlock
type ExtendedAnthropicContent = string | ExtendedContentBlockParam[]
function isThoughtSignatureContentBlock(block: ExtendedContentBlockParam): block is ThoughtSignatureContentBlock {
return block.type === "thoughtSignature"
}
export function convertAnthropicContentToGemini(
content: ExtendedAnthropicContent,
options?: { includeThoughtSignatures?: boolean },
): Part[] {
const includeThoughtSignatures = options?.includeThoughtSignatures ?? true
if (typeof content === "string") {
return [{ text: content }]
}
return content.flatMap((block): Part | Part[] => {
// Handle thoughtSignature blocks first so that the main switch can continue
// to operate on the standard Anthropic content union. This preserves strong
// typing for known block types while still allowing provider-specific
// extensions when needed.
if (isThoughtSignatureContentBlock(block)) {
if (includeThoughtSignatures && typeof block.thoughtSignature === "string") {
// The Google GenAI SDK currently exposes thoughtSignature as an
// extension field on Part; model it structurally without widening
// the upstream type.
return { thoughtSignature: block.thoughtSignature } as Part
}
// Explicitly omit thoughtSignature when not including it.
return []
}
switch (block.type) {
case "text":
return { text: block.text }
@ -70,9 +102,12 @@ export function convertAnthropicContentToGemini(content: string | Anthropic.Cont
})
}
export function convertAnthropicMessageToGemini(message: Anthropic.Messages.MessageParam): Content {
export function convertAnthropicMessageToGemini(
message: Anthropic.Messages.MessageParam,
options?: { includeThoughtSignatures?: boolean },
): Content {
return {
role: message.role === "assistant" ? "model" : "user",
parts: convertAnthropicContentToGemini(message.content),
parts: convertAnthropicContentToGemini(message.content, options),
}
}

View file

@ -129,16 +129,15 @@ export function getModelParams({
temperature = 1.0
} else if (shouldUseReasoningEffort({ model, settings })) {
// "Traditional" reasoning models use the `reasoningEffort` parameter.
const effort = (customReasoningEffort ?? model.reasoningEffort) as any
// Do not propagate "disable" into model params; treat as omission
const effort = (customReasoningEffort ?? model.reasoningEffort) as
| ReasoningEffortExtended
| "disable"
| undefined
// Capability and settings checks are handled by shouldUseReasoningEffort.
// Here we simply propagate the resolved effort into the params, while
// still treating "disable" as an omission.
if (effort && effort !== "disable") {
if (model.supportsReasoningEffort === true) {
// Boolean capability: accept extended efforts; UI still exposes low/medium/high by default
reasoningEffort = effort as ReasoningEffortExtended
} else {
// Array capability: honor exactly what's defined by the model
reasoningEffort = effort as ReasoningEffortExtended
}
reasoningEffort = effort as ReasoningEffortExtended
}
}

View file

@ -21,7 +21,9 @@ export type AnthropicReasoningParams = BetaThinkingConfigParam
export type OpenAiReasoningParams = { reasoning_effort: OpenAI.Chat.ChatCompletionCreateParams["reasoning_effort"] }
export type GeminiReasoningParams = GenerateContentConfig["thinkingConfig"]
export type GeminiReasoningParams = GenerateContentConfig["thinkingConfig"] & {
thinkingLevel?: "low" | "high"
}
export type GetModelReasoningOptions = {
model: ModelInfo
@ -98,14 +100,43 @@ export const getOpenAiReasoning = ({
if (reasoningEffort === "disable" || !reasoningEffort) return undefined
// Include "none" | "minimal" | "low" | "medium" | "high" literally
return { reasoning_effort: reasoningEffort as any }
return {
reasoning_effort: reasoningEffort as OpenAI.Chat.ChatCompletionCreateParams["reasoning_effort"],
}
}
export const getGeminiReasoning = ({
model,
reasoningBudget,
reasoningEffort,
settings,
}: GetModelReasoningOptions): GeminiReasoningParams | undefined =>
shouldUseReasoningBudget({ model, settings })
? { thinkingBudget: reasoningBudget!, includeThoughts: true }
: undefined
}: GetModelReasoningOptions): GeminiReasoningParams | undefined => {
// Budget-based (2.5) models: use thinkingBudget, not thinkingLevel.
if (shouldUseReasoningBudget({ model, settings })) {
return { thinkingBudget: reasoningBudget!, includeThoughts: true }
}
// If reasoning effort shouldn't be used (toggle off, unsupported capability, etc.),
// do not send a thinkingConfig at all.
if (!shouldUseReasoningEffort({ model, settings })) {
return undefined
}
// Effort-based models on Google GenAI: only support explicit low/high levels.
const selectedEffort = (settings.reasoningEffort ?? model.reasoningEffort) as
| ReasoningEffortExtended
| "disable"
| undefined
// Respect “off” / unset semantics.
if (!selectedEffort || selectedEffort === "disable") {
return undefined
}
// Only map "low" and "high" to thinkingLevel; ignore other values.
if (selectedEffort !== "low" && selectedEffort !== "high") {
return undefined
}
return { thinkingLevel: selectedEffort, includeThoughts: true }
}

View file

@ -608,16 +608,18 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
}
private async addToApiConversationHistory(message: Anthropic.MessageParam) {
// Capture the encrypted_content from the provider (e.g., OpenAI Responses API) if present.
// Capture the encrypted_content / thought signatures from the provider (e.g., OpenAI Responses API, Google GenAI) if present.
// We only persist data reported by the current response body.
const handler = this.api as ApiHandler & {
getResponseId?: () => string | undefined
getEncryptedContent?: () => { encrypted_content: string; id?: string } | undefined
getThoughtSignature?: () => string | undefined
}
if (message.role === "assistant") {
const responseId = handler.getResponseId?.()
const reasoningData = handler.getEncryptedContent?.()
const thoughtSignature = handler.getThoughtSignature?.()
// Start from the original assistant message
const messageWithTs: any = {
@ -649,6 +651,26 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
}
}
// If we have a thought signature, append it as a dedicated content block
// so it can be round-tripped in api_history.json and re-sent on subsequent calls.
if (thoughtSignature) {
const thoughtSignatureBlock = {
type: "thoughtSignature",
thoughtSignature,
}
if (typeof messageWithTs.content === "string") {
messageWithTs.content = [
{ type: "text", text: messageWithTs.content } satisfies Anthropic.Messages.TextBlockParam,
thoughtSignatureBlock,
]
} else if (Array.isArray(messageWithTs.content)) {
messageWithTs.content = [...messageWithTs.content, thoughtSignatureBlock]
} else if (!messageWithTs.content) {
messageWithTs.content = [thoughtSignatureBlock]
}
}
this.apiConversationHistory.push(messageWithTs)
} else {
const messageWithTs = { ...message, ts: Date.now() }

View file

@ -460,7 +460,7 @@
"@anthropic-ai/vertex-sdk": "^0.7.0",
"@aws-sdk/client-bedrock-runtime": "^3.922.0",
"@aws-sdk/credential-providers": "^3.922.0",
"@google/genai": "^1.0.0",
"@google/genai": "^1.29.1",
"@lmstudio/sdk": "^1.1.1",
"@mistralai/mistralai": "^1.9.18",
"@modelcontextprotocol/sdk": "1.12.0",