From cd463c0ecf32554aaf4cf94c68d2d891243d791e Mon Sep 17 00:00:00 2001 From: Jack Wotherspoon Date: Tue, 6 Jan 2026 11:18:38 -0500 Subject: [PATCH 1/4] fix: fallback token estimate for images --- .../core/src/utils/tokenCalculation.test.ts | 22 +++++++++++++++++-- packages/core/src/utils/tokenCalculation.ts | 20 +++++++++++++---- 2 files changed, 36 insertions(+), 6 deletions(-) diff --git a/packages/core/src/utils/tokenCalculation.test.ts b/packages/core/src/utils/tokenCalculation.test.ts index 7e1eae3e88d..c6e54bc8870 100644 --- a/packages/core/src/utils/tokenCalculation.test.ts +++ b/packages/core/src/utils/tokenCalculation.test.ts @@ -123,8 +123,26 @@ describe('calculateRequestTokenCount', () => { // Should fallback to estimation: // 'Hello': 5 chars * 0.25 = 1.25 - // inlineData: JSON.stringify length / 4 - expect(count).toBeGreaterThan(0); + // inlineData: 3000 + // Total: 3001.25 -> 3001 + expect(count).toBe(3001); expect(mockContentGenerator.countTokens).toHaveBeenCalled(); }); + + it('should use fixed estimate for images in fallback', async () => { + vi.mocked(mockContentGenerator.countTokens).mockRejectedValue( + new Error('API error'), + ); + const request = [ + { inlineData: { mimeType: 'image/png', data: 'large_data' } }, + ]; + + const count = await calculateRequestTokenCount( + request, + mockContentGenerator, + model, + ); + + expect(count).toBe(3000); + }); }); diff --git a/packages/core/src/utils/tokenCalculation.ts b/packages/core/src/utils/tokenCalculation.ts index 0359cb3e7c0..4c99eb1c463 100644 --- a/packages/core/src/utils/tokenCalculation.ts +++ b/packages/core/src/utils/tokenCalculation.ts @@ -31,10 +31,22 @@ export function estimateTokenCountSync(parts: Part[]): number { } } } else { - // For non-text parts (functionCall, functionResponse, executableCode, etc.), - // we fallback to the JSON string length heuristic. - // Note: This is an approximation. - totalTokens += JSON.stringify(part).length / 4; + // For images we use a safe fallback + const inlineData = 'inlineData' in part ? part.inlineData : undefined; + const fileData = 'fileData' in part ? part.fileData : undefined; + const mimeType = inlineData?.mimeType || fileData?.mimeType; + + if (mimeType?.startsWith('image/')) { + // For images, we use a fixed safe estimate (3,000 tokens) covering + // up to 4K resolution on Gemini 3. + // See: https://ai.google.dev/gemini-api/docs/vision#token_counting + totalTokens += 3000; + } else { + // For other non-text parts (functionCall, functionResponse, etc.), + // we fallback to the JSON string length heuristic. + // Note: This is an approximation. + totalTokens += JSON.stringify(part).length / 4; + } } } return Math.floor(totalTokens); From f7407e1cf1b0b899a318b62f35f73ffda46fdc70 Mon Sep 17 00:00:00 2001 From: Jack Wotherspoon Date: Tue, 6 Jan 2026 11:24:49 -0500 Subject: [PATCH 2/4] chore: update comments --- packages/core/src/utils/tokenCalculation.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/core/src/utils/tokenCalculation.ts b/packages/core/src/utils/tokenCalculation.ts index 4c99eb1c463..a61ae83c474 100644 --- a/packages/core/src/utils/tokenCalculation.ts +++ b/packages/core/src/utils/tokenCalculation.ts @@ -31,15 +31,14 @@ export function estimateTokenCountSync(parts: Part[]): number { } } } else { - // For images we use a safe fallback + // For images, we use a fixed safe estimate (3,000 tokens) covering + // up to 4K resolution on Gemini 3. + // See: https://ai.google.dev/gemini-api/docs/vision#token_counting const inlineData = 'inlineData' in part ? part.inlineData : undefined; const fileData = 'fileData' in part ? part.fileData : undefined; const mimeType = inlineData?.mimeType || fileData?.mimeType; if (mimeType?.startsWith('image/')) { - // For images, we use a fixed safe estimate (3,000 tokens) covering - // up to 4K resolution on Gemini 3. - // See: https://ai.google.dev/gemini-api/docs/vision#token_counting totalTokens += 3000; } else { // For other non-text parts (functionCall, functionResponse, etc.), From 73b5c9e16ff11285183454d926f31467c8c102c5 Mon Sep 17 00:00:00 2001 From: Jack Wotherspoon Date: Tue, 6 Jan 2026 12:31:27 -0500 Subject: [PATCH 3/4] chore: resolve model --- packages/core/src/core/client.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/core/src/core/client.ts b/packages/core/src/core/client.ts index ecd1eff471b..3e758b24007 100644 --- a/packages/core/src/core/client.ts +++ b/packages/core/src/core/client.ts @@ -60,6 +60,7 @@ import { applyModelSelection, createAvailabilityContextProvider, } from '../availability/policyHelpers.js'; +import { resolveModel } from '../config/models.js'; import type { RetryAvailabilityContext } from '../utils/retry.js'; const MAX_TURNS = 100; @@ -508,7 +509,9 @@ export class GeminiClient { // Availability logic: The configured model is the source of truth, // including any permanent fallbacks (config.setModel) or manual overrides. - return this.config.getActiveModel(); + // Resolve auto model names (e.g., "auto-gemini-3") to concrete model names + // (e.g., "gemini-3-pro-preview") so that API calls like countTokens work correctly. + return resolveModel(this.config.getActiveModel()); } private async *processTurn( From 7886f55ee6db25c29c015a4f4efb75b96d414332 Mon Sep 17 00:00:00 2001 From: Jack Wotherspoon Date: Tue, 6 Jan 2026 14:05:39 -0500 Subject: [PATCH 4/4] chore: add debugLogger for failed token count api --- packages/core/src/core/client.ts | 2 -- packages/core/src/utils/tokenCalculation.ts | 8 ++++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/core/src/core/client.ts b/packages/core/src/core/client.ts index 3e758b24007..48da7e43e7e 100644 --- a/packages/core/src/core/client.ts +++ b/packages/core/src/core/client.ts @@ -509,8 +509,6 @@ export class GeminiClient { // Availability logic: The configured model is the source of truth, // including any permanent fallbacks (config.setModel) or manual overrides. - // Resolve auto model names (e.g., "auto-gemini-3") to concrete model names - // (e.g., "gemini-3-pro-preview") so that API calls like countTokens work correctly. return resolveModel(this.config.getActiveModel()); } diff --git a/packages/core/src/utils/tokenCalculation.ts b/packages/core/src/utils/tokenCalculation.ts index a61ae83c474..06292bb9254 100644 --- a/packages/core/src/utils/tokenCalculation.ts +++ b/packages/core/src/utils/tokenCalculation.ts @@ -6,6 +6,7 @@ import type { PartListUnion, Part } from '@google/genai'; import type { ContentGenerator } from '../core/contentGenerator.js'; +import { debugLogger } from './debugLogger.js'; // Token estimation constants // ASCII characters (0-127) are roughly 4 chars per token @@ -13,6 +14,8 @@ const ASCII_TOKENS_PER_CHAR = 0.25; // Non-ASCII characters (including CJK) are often 1-2 tokens per char. // We use 1.3 as a conservative estimate to avoid underestimation. const NON_ASCII_TOKENS_PER_CHAR = 1.3; +// Fixed token estimate for images +const IMAGE_TOKEN_ESTIMATE = 3000; /** * Estimates token count for parts synchronously using a heuristic. @@ -39,7 +42,7 @@ export function estimateTokenCountSync(parts: Part[]): number { const mimeType = inlineData?.mimeType || fileData?.mimeType; if (mimeType?.startsWith('image/')) { - totalTokens += 3000; + totalTokens += IMAGE_TOKEN_ESTIMATE; } else { // For other non-text parts (functionCall, functionResponse, etc.), // we fallback to the JSON string length heuristic. @@ -80,8 +83,9 @@ export async function calculateRequestTokenCount( contents: [{ role: 'user', parts }], }); return response.totalTokens ?? 0; - } catch { + } catch (error) { // Fallback to local estimation if the API call fails + debugLogger.debug('countTokens API failed:', error); return estimateTokenCountSync(parts); } }